上篇文章説到,之前使用Redis的String數據結構進行存儲異步上傳石墨文檔的任務狀態,做法有些性能上的問題。
下面簡單列舉一下采用String數據結構進行存儲的劣勢:
- 缺少歷史記錄:無法追蹤任務執行的完整過程、只能獲取最新狀態,丟失中間狀態信息
- 併發處理:在高併發場景下需要額外考慮樂觀鎖等機制避免數據覆蓋、需要使用
WATCH命令或Lua腳本確保原子性- 功能侷限:不支持隊列操作,無法實現基於隊列的分佈式處理、不適合需要按順序處理的場景
採用Redis的LIst數據結構或者String數據結構如何選擇?
適合使用List數據結構
- 需要完整記錄任務執行歷史
- 需要按時間順序查看任務狀態變化
- 任務執行次數有限,存儲空間不是主要考慮因素
- 需要支持分佈式任務處理
適合使用String數據結構
- 任務更新頻繁,存儲空間是關鍵考慮因素
- 系統併發量大,需要最高的讀寫性能
- 只關注任務的最新狀態
- 任務狀態簡單,不需要複雜的歷史記錄
進度條實現邏輯簡圖
下圖簡單説明了進度條大致的邏輯,進度條的更新進度和具體業務的步驟進行綁定,當然下圖是主流程簡化版本。
完整流程邏輯圖
如何使用Redis的List結構進行操作
創建一個操作Redis的工具類,需要在工具類中定義於業務相關的屬性字段信息,定義多個構造方法進行存儲需要更新字段信息。很關鍵需要直接使用對象進行直接存儲,避免採用JSON格式化方式,JSON格式化,讀-修改-寫問題:當多個線程同時讀取、修改和寫入同一JSON時,可能導致數據不一。部分更新問題:當只需更新對象的部分字段時,使用JSON需要先讀取整個對象,再修改,再寫回。
利用Redis的List數據結構存儲
/**
* 將任務狀態添加到Redis列表中
* @param redisTemplate Redis模板
*/
public void addTaskToList(RedisTemplate<String, Object> redisTemplate) {
String taskKey = this.findTaskCacheKey();
redisTemplate.opsForList().leftPush(taskKey, this);
redisTemplate.expire(taskKey, 1, TimeUnit.DAYS);
}
/**
* 更新任務進度
* @param status 狀態
* @param msg 消息
* @param addPercent 增加的進度百分比
* @param redisTemplate Redis模板(使用這個進行存儲調用)
*/
public void commonUpdate(String status, String msg, Integer addPercent, RedisTemplate<String, Object> redisTemplate) {
this.commonUpdate(status, msg, addPercent, null, redisTemplate);
}
/**
* 從Redis中清理任務進度記錄
* @param taskId 任務ID
* @param userCode 用户編碼
* @param targetStatus 目標狀態(SUCCESS或FAILED)- 只保留這個狀態的記錄,若為null則刪除所有記錄
*/
private void clearTaskProgressRecords(String taskId, String userCode, String targetStatus) {
try {
// 獲取用户任務列表的鍵
String userTasksKey = String.format("%s:%s", TASK_PROCESS_PREFIX_KEY, userCode);
// 獲取當前任務列表
List<Object> tasksList = redisTemplate.opsForList().range(userTasksKey, 0, -1);
if (tasksList != null && !tasksList.isEmpty()) {
// 收集需要刪除的元素和需要保留的元素
List<Object> toRemove = new ArrayList<>();
Object targetRecord = null;
for (Object taskObj : tasksList) {
try {
// 檢查對象類型
if (taskObj instanceof xxxx) {
LongTaskProcessResponse task = (xxx) taskObj;
String currentTaskId = task.getTaskId();
String status = task.getStatus();
// 如果找到匹配的任務ID
if (taskId.equals(currentTaskId)) {
// 如果指定了目標狀態,檢查是否匹配
if (targetStatus != null && targetStatus.equals(status)) {
// 保留目標狀態的記錄
targetRecord = taskObj;
} else {
// 刪除非目標狀態的記錄
toRemove.add(taskObj);
}
}
} else {
log.warn("任務對象類型不正確,無法處理:{}",
taskObj != null ? taskObj.getClass().getName() : "null");
}
} catch (Exception e) {
log.warn("處理任務對象失敗: {}", e.getMessage());
}
}
// 刪除收集到的所有元素
for (Object obj : toRemove) {
redisTemplate.opsForList().remove(userTasksKey, 0, obj);
}
// 如果目標記錄存在,確保它在列表的最前面(最新)
if (targetRecord != null) {
// 先刪除,再添加到列表頭部,確保是最新的記錄
redisTemplate.opsForList().remove(userTasksKey, 0, targetRecord);
redisTemplate.opsForList().leftPush(userTasksKey, targetRecord);
}
if (!toRemove.isEmpty()) {
log.info("從Redis中清理任務進度記錄,userCode: {}, taskId: {}, 刪除記錄數: {}, 保留狀態: {}",
userCode, taskId, toRemove.size(), targetStatus);
}
}
} catch (Exception e) {
log.error("從Redis中清理任務進度記錄失敗,taskId: {}, userCode: {}", taskId, userCode, e);
}
}
利用Java特性進行存儲Redis
this自動引用的就是調用該方法的failedResponse對象- 方法中的
this不需要顯式傳遞,它是Java方法調用機制自動提供的 - 當執行
leftPush(taskKey, this)時,傳入Redis的就是整個failedResponse對象
// 創建一個純粹的失敗狀態記錄
LongTaskProcessResponse failedResponse = new LongTaskProcessResponse();
failedResponse.setTaskId(subTaskResponse.getTaskId());
failedResponse.setBusinessType(subTaskResponse.getBusinessType());
failedResponse.setStatus(KbProcessStatus.FAILED.name());
failedResponse.setMsg("處理失敗: " + e.getMessage());
failedResponse.setProcessPercent(new BigDecimal(0));
failedResponse.setUserCode(currentUser.getCode());
failedResponse.setTitle(subTaskResponse.getTitle());
failedResponse.setCreateTime(System.currentTimeMillis());
failedResponse.setExtraData(subTaskResponse.getExtraData());
// 添加失敗記錄
failedResponse.addTaskToList(redisTemplate);
/**
* 將任務狀態添加到Redis列表中
* @param redisTemplate Redis模板
*/
public void addTaskToList(RedisTemplate<String, Object> redisTemplate) {
String taskKey = this.findTaskCacheKey();
redisTemplate.opsForList().leftPush(taskKey, this);
redisTemplate.expire(taskKey, 1, TimeUnit.DAYS);
}