博客 / 詳情

返回

異步上傳石墨文件進度條前端展示記錄(採用Redis中List數據結構實現)

上篇文章説到,之前使用Redis的String數據結構進行存儲異步上傳石墨文檔的任務狀態,做法有些性能上的問題。

下面簡單列舉一下采用String數據結構進行存儲的劣勢:

  1. 缺少歷史記錄:無法追蹤任務執行的完整過程、只能獲取最新狀態,丟失中間狀態信息
  2. 併發處理:在高併發場景下需要額外考慮樂觀鎖等機制避免數據覆蓋、需要使用WATCH命令或Lua腳本確保原子性
  3. 功能侷限:不支持隊列操作,無法實現基於隊列的分佈式處理、不適合需要按順序處理的場景

採用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);
    }
user avatar fulng 頭像 yexiaobai_616e2b70869ae 頭像 dolphinscheduler 頭像 tina_tang 頭像 anan_5ca066790c21a 頭像
5 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.