Hi,大家好,我是灰小猿!
在一些功能的开发中,我们一般会有一些场景需要将得到的数据先暂时的存储起来,以便后面的接口或业务使用,这种场景我们一般常用的场景就是将数据暂时存储在缓存中,之后再从缓存获取,以支持高可用的分布式项目为例,可以通过以下步骤实现数据的临时存储和后续处理:
举例场景及解决方案
举例场景:以用户导入文件并解析文件数据,响应变更数据给用户,确认数据无误后存储的场景为例,需要对应两个接口:
文件解析接口:用户导入文件并解析文件数据,响应变更数据给用户
数据存储接口:确认数据无误后存储上一接口解析出来的文件数据
使用 Redis 临时存储文件解析出的DTO数据,结合 唯一Token标识 确保两次请求间的数据关联。具体步骤如下:
1. 用户上传文件并解析
接口设计
-
请求方式:
POST /api/upload
-
参数:
MultipartFile file
-
返回:解析数据变更信息 + 唯一Token(用于后续操作)
代码实现
@PostMapping("/upload")
public ResponseEntity<ConflictResponse> handleFileUpload(@RequestParam("file") MultipartFile file) throws IOException {// 1. 解析文件生成DTOList<DataDTO> parsedData = fileParser.parse(file.getInputStream());// 2. 与数据库对比,生成冲突信息List<ConflictInfo> conflicts = dataComparator.compareWithDatabase(parsedData);// 3. 生成唯一Token(如UUID)String token = UUID.randomUUID().toString();// 4. 将DTO数据存入Redis,设置过期时间(如30分钟)redisTemplate.opsForValue().set("upload:data:" + token, parsedData, Duration.ofMinutes(30));// 5. 返回冲突信息和Tokenreturn ResponseEntity.ok(new ConflictResponse(conflicts, token));
}
2. 用户提交处理选择
接口设计
-
请求方式:
POST /api/resolve
-
参数:
ResolveRequest
(包含Token和用户选择) -
返回:处理结果
代码实现
@PostMapping("/resolve")
public ResponseEntity<String> resolveConflicts(@RequestBody ResolveRequest request) {// 1. 从Redis中获取临时存储的DTO数据String redisKey = "upload:data:" + request.getToken();List<DataDTO> parsedData = (List<DataDTO>) redisTemplate.opsForValue().get(redisKey);if (parsedData == null) {return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("操作超时或Token无效,请重新上传文件");}// 2. ******中间业务数据处理******// 3. 清理Redis中的临时数据redisTemplate.delete(redisKey);return ResponseEntity.ok("数据处理完成");
}
3. 关键组件说明
(1) Redis配置
确保Spring Boot项目已集成Redis,配置连接信息:
spring:redis:host: localhostport: 6379password: timeout: 5000
(2) DTO序列化
确保DTO类实现Serializable
接口,或使用JSON序列化:
public class DataDTO implements Serializable {private String field1;private int field2;// getters/setters
}
(3) 安全性优化
-
Token生成:使用
UUID
或JWT保证唯一性和安全性。 -
数据加密:若DTO包含敏感信息,可在存储到Redis前加密。
以上是正常的在Redis存储临时数据的一个完整过程,属于比较基本的操作,但是倘若我们存储的数据比较大,那么在存储数据到redis的时候就会出现一些内存溢出或超时等异常,所以下面是主要针对这种数据场景的一些处理方案。
4. 处理大数据量的优化【重点】
如果文件解析后的DTO数据量极大(如超过10MB),需优化存储和传输:以下是我总结的一些常用的数据存储方案。
(1) 分片存储
将数据拆分为多个块存入Redis,避免单键过大:
// 存储分片
for (int i = 0; i < parsedData.size(); i += CHUNK_SIZE) {List<DataDTO> chunk = parsedData.subList(i, Math.min(i + CHUNK_SIZE, parsedData.size()));redisTemplate.opsForList().rightPushAll("upload:data:" + token + ":chunks", chunk);
}// 读取分片
List<DataDTO> allData = new ArrayList<>();
while (redisTemplate.opsForList().size(redisKey) > 0) {List<DataDTO> chunk = redisTemplate.opsForList().leftPop(redisKey);allData.addAll(chunk);
}
(2) 压缩数据【推荐】
在存储到Redis前对数据进行压缩(如GZIP),
// 压缩
ByteArrayOutputStream bos = new ByteArrayOutputStream();
GZIPOutputStream gzip = new GZIPOutputStream(bos);
ObjectOutputStream oos = new ObjectOutputStream(gzip);
oos.writeObject(parsedData);
oos.close();
byte[] compressedData = bos.toByteArray();
redisTemplate.opsForValue().set(redisKey, compressedData);// 解压
byte[] compressedData = (byte[]) redisTemplate.opsForValue().get(redisKey);
ByteArrayInputStream bis = new ByteArrayInputStream(compressedData);
GZIPInputStream gzip = new GZIPInputStream(bis);
ObjectInputStream ois = new ObjectInputStream(gzip);
List<DataDTO> parsedData = (List<DataDTO>) ois.readObject();
5. 异常处理
(1) Token过期或无效
在从redis中获取缓存数据的时候,要考虑到Redis中数据是否已经过期等问题,并且针对相应的情况作出返回错误提示,要求用户重新上传文件:
if (parsedData == null) {return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("操作超时或Token无效,请重新上传文件");
}
(2) 数据反序列化失败
在从redis获取到数据json,将其反序列化为具体对象时,如果你序列化和反序列化使用的方式不同,可能会出现反序列化失败的问题,所以针对可能出现的这种情况,一般建议捕获异常并记录日志:
try {List<DataDTO> parsedData = (List<DataDTO>) redisTemplate.opsForValue().get(redisKey);
} catch (SerializationException e) {logger.error("反序列化失败: {}", e.getMessage());return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("数据处理错误");
}
在上面存储数据的时候,如果你的对象嵌套比较复杂,那么还有可能会出现下面的问题,这也是我在存储复杂对象数据到Redis时遇到的一个问题
Java对象存储到Redis报StackOverflowError错误解决
在Java中将对象存储到Redis时遇到StackOverflowError
错误,通常是由于对象之间存在循环引用导致序列化时无限递归,以下是逐步解决方案:
1. 确认错误原因
检查异常堆栈跟踪,确认是否在序列化过程中触发StackOverflowError
。典型场景:
com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion (StackOverflowError)
2. 解决循环引用问题
方案一:使用 @JsonIgnore
忽略循环字段
在可能引发循环引用的字段上添加注解,阻止其序列化,不过这种方式要确认你忽略的字段确实是不需要序列化的,否则这个属性值会在序列化后丢失。
public class User {private String name;@JsonIgnore // 忽略此字段的序列化private User friend;// getters/setters
}
方案二:使用 @JsonManagedReference
和 @JsonBackReference【推荐】
通常引起上面问题的主要原因就是数据模型在定义的过程中出现了数据循环递归的情况,导致数据无限的序列化下去,在这里可以通过使用这两个注解来明确父子关系,避免无限递归:
public class Parent {private String name;@JsonManagedReference // 标记为“主”引用private List<Child> children;// getters/setters
}public class Child {private String name;@JsonBackReference // 标记为“反向”引用private Parent parent;// getters/setters
}
方案三:配置 Jackson 忽略循环引用
在 ObjectMapper
中配置,允许忽略循环引用:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.FAIL_ON_SELF_REFERENCES, false);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
3. 使用 Redis 序列化器避免递归
如果使用 Spring Data Redis,建议更换为 GenericJackson2JsonRedisSerializer
,并配置其处理循环引用:
@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(factory);// 使用 Jackson 序列化器ObjectMapper objectMapper = new ObjectMapper();objectMapper.enable(SerializationFeature.INDENT_OUTPUT);objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);return template;}
}
4. 优化对象结构
如果无法修改源码,可创建 DTO(数据传输对象),仅序列化必要字段:
public class UserDTO {private String name;// 不包含 friend 字段public UserDTO(User user) {this.name = user.getName();}// getters/setters
}
其他推荐(本地缓存caffeine)
如果你的系统不需要考虑高可用和分布式,那么对比使用Redis缓存来存储临时数据,我更推荐使用本地缓存caffeine来存储,
这种方式不仅不需要将对象进行序列化和反序列化,而且可以有效避免大数据对象存储和获取时存在的性能问题,因为它是完全基于内存来实现的,方便易用。且几乎没有性能损耗。
总结
在通过Redis存储大量数据时,推荐使用压缩和解压缩的形式进行存储。
在存储复杂对象时,建议提前确认对象之间是否存在嵌套引用的情况,如果存在这种情况,建议确认数据模型定义是否合理,如果数据模型定义不合理,建议优先选择优化数据模型,否则建议使用 @JsonManagedReference
和 @JsonBackReference注解
来标明主从结构,从而避免
对象之间存在循环引用,导致序列化时无限递归的问题。