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注解
來標明主從結構,從而避免
對象之間存在循環引用,導致序列化時無限遞歸的問題。