【Easylive】項目常見問題解答(自用&持續更新中…) 匯總版
這個定時任務系統主要包含兩個核心功能:每日數據統計和臨時文件清理。下面我將詳細解析這兩個定時任務的實現邏輯和技術要點:
@Component
@Slf4j
public class SysTask {@Resourceprivate StatisticsInfoService statisticsInfoService;@Resourceprivate AppConfig appConfig;@Scheduled(cron = "0 0 0 * * ?")public void statisticsData() {statisticsInfoService.statisticsData();}@Scheduled(cron = "0 */1 * * * ?")public void delTempFile() {String tempFolderName = appConfig.getProjectFolder() + Constants.FILE_FOLDER + Constants.FILE_FOLDER_TEMP;File folder = new File(tempFolderName);File[] listFile = folder.listFiles();if (listFile == null) {return;}String twodaysAgo = DateUtil.format(DateUtil.getDayAgo(2), DateTimePatternEnum.YYYYMMDD.getPattern()).toLowerCase();Integer dayInt = Integer.parseInt(twodaysAgo);for (File file : listFile) {Integer fileDate = Integer.parseInt(file.getName());if (fileDate <= dayInt) {try {FileUtils.deleteDirectory(file);} catch (IOException e) {log.info("刪除臨時文件失敗", e);}}}}
}
一、statisticsData()
每日數據統計任務
執行時機:每天凌晨0點(0 0 0 * * ?
)
1. 數據統計流程
2. 核心代碼解析
public void statisticsData() {// 1. 準備數據結構List<StatisticsInfo> statisticsInfoList = new ArrayList<>();final String statisticsDate = DateUtil.getBeforeDayDate(1); // 獲取昨天的日期// 2. 播放量統計(Redis → DB)Map<String, Integer> videoPlayCountMap = redisComponent.getVideoPlayCount(statisticsDate);// 處理視頻ID格式List<String> playVideoKeys = videoPlayCountMap.keySet().stream().map(item -> item.substring(item.lastIndexOf(":") + 1)).collect(Collectors.toList());// 3. 關聯用戶信息VideoInfoQuery query = new VideoInfoQuery();query.setVideoIdArray(playVideoKeys.toArray(new String[0]));List<VideoInfo> videoInfoList = videoInfoMapper.selectList(query);// 4. 按用戶聚合播放量Map<String, Integer> videoCountMap = videoInfoList.stream().collect(Collectors.groupingBy(VideoInfo::getUserId,Collectors.summingInt(item -> videoPlayCountMap.getOrDefault(Constants.REDIS_KEY_VIDEO_PLAY_COUNT + statisticsDate + ":" + item.getVideoId(), 0))));// 5. 構建播放統計對象videoCountMap.forEach((userId, count) -> {StatisticsInfo info = new StatisticsInfo();info.setStatisticsDate(statisticsDate);info.setUserId(userId);info.setDataType(StatisticsTypeEnum.PLAY.getType());info.setStatisticsCount(count);statisticsInfoList.add(info);});// 6. 補充其他維度數據addFansData(statisticsDate, statisticsInfoList); // 粉絲統計addCommentData(statisticsDate, statisticsInfoList); // 評論統計addInteractionData(statisticsDate, statisticsInfoList); // 點贊/收藏/投幣// 7. 批量入庫statisticsInfoMapper.insertOrUpdateBatch(statisticsInfoList);
}
3. 關鍵技術點
-
Redis+DB混合統計:
? 播放量等高頻數據先記錄到Redis
? 定時任務從Redis獲取昨日數據后持久化到DB -
多維度統計:
? 播放量:基于視頻ID聚合后關聯用戶
? 粉絲量:直接查詢DB關系數據
? 互動數據:統一處理點贊/收藏/投幣等行為 -
批量操作優化:
? 使用insertOrUpdateBatch
實現批量upsert
? 減少數據庫連接次數提升性能
二、delTempFile()
臨時文件清理任務
執行時機:每分鐘執行一次(0 */1 * * * ?
)
1. 文件清理邏輯
public void delTempFile() {// 1. 構建臨時文件夾路徑String tempFolder = appConfig.getProjectFolder() + Constants.FILE_FOLDER + Constants.FILE_FOLDER_TEMP;// 2. 獲取兩天前的日期(基準線)String twodaysAgo = DateUtil.format(DateUtil.getDayAgo(2), "yyyyMMdd");Integer thresholdDate = Integer.parseInt(twodaysAgo);// 3. 遍歷臨時文件夾File folder = new File(tempFolder);File[] files = folder.listFiles();if (files == null) return;for (File file : files) {try {// 4. 按日期命名規則清理舊文件Integer fileDate = Integer.parseInt(file.getName());if (fileDate <= thresholdDate) {FileUtils.deleteDirectory(file); // 遞歸刪除}} catch (Exception e) {log.error("刪除臨時文件失敗", e);}}
}
2. 設計要點
-
安全機制:
? 文件名強制使用日期格式(如20230815
)
? 只刪除命名合規的文件夾 -
容錯處理:
? 捕獲IOException
防止單次失敗影響后續操作
? 空文件夾自動跳過 -
性能考慮:
? 高頻檢查(每分鐘)但低負載(僅處理過期文件)
? 使用FileUtils
工具類保證刪除可靠性
三、架構設計亮點
-
解耦設計:
? 統計服務與業務邏輯分離
? 文件清理與業務模塊隔離 -
數據一致性:
? 實時數據寫入Redis保證性能
? 定時同步到DB保證持久化 -
擴展性:
? 新增統計維度只需添加對應方法
? 文件清理策略可配置化
四、潛在優化建議
-
統計任務優化:
// 可考慮分片統計(大用戶量場景) @Scheduled(cron = "0 0 1 * * ?") public void statsUserShard1() {statisticsService.processByUserRange(0, 10000); }
-
文件清理增強:
// 添加文件大小監控 if (file.length() > MAX_TEMP_FILE_SIZE) {alertService.notifyOversizeFile(file); }
-
異常處理強化:
@Scheduled(...) public void safeStatistics() {try {statisticsData();} catch (Exception e) {log.error("統計任務失敗", e);retryLater(); // 延遲重試機制} }
這套定時任務系統通過合理的職責劃分和穩健的實現方式,有效解決了數據統計和資源清理這兩類經典的后臺任務需求。
@Override
public void statisticsData() {// 創建統計結果容器List<StatisticsInfo> statisticsInfoList = new ArrayList<>();// 獲取統計日期(昨天)final String statisticsDate = DateUtil.getBeforeDayDate(1);// ========== 播放量統計 ==========// 從Redis獲取昨日所有視頻的播放量數據// 格式:Map<"video_play_count:20230815:video123", 播放次數>Map<String, Integer> videoPlayCountMap = redisComponent.getVideoPlayCount(statisticsDate);// 提取視頻ID列表(去掉前綴)List<String> playVideoKeys = new ArrayList<>(videoPlayCountMap.keySet());playVideoKeys = playVideoKeys.stream().map(item -> item.substring(item.lastIndexOf(":") + 1)) // 截取最后一個:后的視頻ID.collect(Collectors.toList());// 構建視頻查詢條件VideoInfoQuery videoInfoQuery = new VideoInfoQuery();videoInfoQuery.setVideoIdArray(playVideoKeys.toArray(new String[playVideoKeys.size()]));// 批量查詢視頻基本信息List<VideoInfo> videoInfoList = videoInfoMapper.selectList(videoInfoQuery);// 按用戶ID分組統計總播放量Map<String, Integer> videoCountMap = videoInfoList.stream().collect(Collectors.groupingBy(VideoInfo::getUserId, // 按用戶ID分組Collectors.summingInt(item -> {// 重組Redis key獲取該視頻播放量String redisKey = Constants.REDIS_KEY_VIDEO_PLAY_COUNT + statisticsDate + ":" + item.getVideoId();Integer count = videoPlayCountMap.get(redisKey);return count == null ? 0 : count; // 空值保護})));// 轉換播放統計結果對象videoCountMap.forEach((userId, count) -> {StatisticsInfo statisticsInfo = new StatisticsInfo();statisticsInfo.setStatisticsDate(statisticsDate); // 統計日期statisticsInfo.setUserId(userId); // 用戶IDstatisticsInfo.setDataType(StatisticsTypeEnum.PLAY.getType()); // 數據類型=播放statisticsInfo.setStatisticsCount(count); // 播放次數statisticsInfoList.add(statisticsInfo); // 加入結果集});// ========== 粉絲量統計 ==========// 從數據庫查詢昨日粉絲變化數據List<StatisticsInfo> fansDataList = this.statisticsInfoMapper.selectStatisticsFans(statisticsDate);// 設置統計維度和日期for (StatisticsInfo statisticsInfo : fansDataList) {statisticsInfo.setStatisticsDate(statisticsDate); // 統一日期格式statisticsInfo.setDataType(StatisticsTypeEnum.FANS.getType()); // 數據類型=粉絲}statisticsInfoList.addAll(fansDataList); // 合并結果// ========== 評論統計 ==========// 從數據庫查詢昨日評論數據List<StatisticsInfo> commentDataList = this.statisticsInfoMapper.selectStatisticsComment(statisticsDate);// 設置統計維度和日期for (StatisticsInfo statisticsInfo : commentDataList) {statisticsInfo.setStatisticsDate(statisticsDate); // 統一日期格式statisticsInfo.setDataType(StatisticsTypeEnum.COMMENT.getType()); // 數據類型=評論}statisticsInfoList.addAll(commentDataList); // 合并結果// ========== 互動行為統計 ==========// 查詢點贊/收藏/投幣數據(參數為行為類型數組)List<StatisticsInfo> statisticsInfoOthers = this.statisticsInfoMapper.selectStatisticsInfo(statisticsDate,new Integer[]{UserActionTypeEnum.VIDEO_LIKE.getType(), // 點贊UserActionTypeEnum.VIDEO_COIN.getType(), // 投幣UserActionTypeEnum.VIDEO_COLLECT.getType() // 收藏});// 轉換行為類型為統計類型for (StatisticsInfo statisticsInfo : statisticsInfoOthers) {statisticsInfo.setStatisticsDate(statisticsDate); // 統一日期格式// 行為類型轉換if (UserActionTypeEnum.VIDEO_LIKE.getType().equals(statisticsInfo.getDataType())) {statisticsInfo.setDataType(StatisticsTypeEnum.LIKE.getType()); // 點贊} else if (UserActionTypeEnum.VIDEO_COLLECT.getType().equals(statisticsInfo.getDataType())) {statisticsInfo.setDataType(StatisticsTypeEnum.COLLECTION.getType()); // 收藏} else if (UserActionTypeEnum.VIDEO_COIN.getType().equals(statisticsInfo.getDataType())) {statisticsInfo.setDataType(StatisticsTypeEnum.COIN.getType()); // 投幣}}statisticsInfoList.addAll(statisticsInfoOthers); // 合并結果// ========== 最終入庫 ==========// 批量插入或更新統計結果this.statisticsInfoMapper.insertOrUpdateBatch(statisticsInfoList);
}
這段代碼使用Java 8的Stream API和Collectors工具類,實現了按用戶ID分組統計視頻總播放量的功能。下面我將從多個維度進行詳細解析:
一、代碼結構分解
Map<String, Integer> videoCountMap = videoInfoList.stream().collect(Collectors.groupingBy(VideoInfo::getUserId, Collectors.summingInt(item -> {String redisKey = Constants.REDIS_KEY_VIDEO_PLAY_COUNT + statisticsDate + ":" + item.getVideoId();Integer count = videoPlayCountMap.get(redisKey);return count == null ? 0 : count;})));
二、逐層解析
1. 數據準備階段
? 輸入:videoInfoList
(視頻信息列表,包含videoId
和userId
等字段)
? 目標:生成Map<用戶ID, 該用戶所有視頻的總播放量>
2. Stream流水線
videoInfoList.stream()
將List轉換為Stream,準備進行流式處理。
3. 核心收集器
Collectors.groupingBy(VideoInfo::getUserId, // 分組依據:用戶IDCollectors.summingInt(...) // 聚合方式:求和
)
? groupingBy
:按用戶ID分組,生成Map<String, List<VideoInfo>>
? summingInt
:對每組內的元素進行整數求和
4. 播放量計算邏輯
item -> {// 重組Redis key:video_play_count:20230815:video123String redisKey = Constants.REDIS_KEY_VIDEO_PLAY_COUNT + statisticsDate + ":" + item.getVideoId();// 從預加載的Map獲取播放量Integer count = videoPlayCountMap.get(redisKey);// 空值保護(沒有記錄則視為0次播放)return count == null ? 0 : count;
}
三、關鍵技術點
1. 嵌套收集器
? 外層:groupingBy
按用戶分組
? 內層:summingInt
對播放量求和
形成兩級聚合操作。
2. 實時Key重組
Constants.REDIS_KEY_VIDEO_PLAY_COUNT + statisticsDate + ":" + item.getVideoId()
動態拼接Redis key,與之前從Redis加載數據時的key格式嚴格一致:
? 常量前綴:video_play_count:
? 日期分區:20230815
? 視頻ID::video123
3. 空值防御
count == null ? 0 : count
處理可能存在的:
? Redis中無該視頻記錄
? 視頻剛上傳尚未有播放數據
四、內存數據流演示
假設原始數據:
videoInfoList = [{videoId: "v1", userId: "u1"}, {videoId: "v2", userId: "u1"},{videoId: "v3", userId: "u2"}
]videoPlayCountMap = {"video_play_count:20230815:v1": 100,"video_play_count:20230815:v2": 50,"video_play_count:20230815:v3": 200
}
執行過程:
- 流處理開始
- 遇到v1(用戶u1)→ 計算播放量100 → u1分組累計100
- 遇到v2(用戶u1)→ 計算播放量50 → u1分組累計150
- 遇到v3(用戶u2)→ 計算播放量200 → u2分組累計200
最終結果:
{u1=150, u2=200}
五、設計優勢
-
高效聚合
單次遍歷完成分組+求和,時間復雜度O(n) -
內存友好
流式處理避免中間集合的創建 -
可維護性
清晰表達業務邏輯:
“按用戶分組,求和每個用戶所有視頻的播放量” -
擴展性
如需修改統計邏輯(如求平均值),只需替換summingInt
為其他收集器
六、潛在優化方向
1. 并行處理(大數據量時)
videoInfoList.parallelStream()...
注意線程安全和順序保證
2. 緩存Key構建
// 預先生成videoId到播放量的映射,避免重復拼接字符串
Map<String, Integer> videoIdToCount = ...;
3. 異常處理增強
try {Integer count = videoPlayCountMap.get(redisKey);return Optional.ofNullable(count).orElse(0);
} catch (Exception e) {log.warn("播放量統計異常 videoId:{}", item.getVideoId());return 0;
}
這段代碼展示了如何優雅地結合Stream API和集合操作,實現復雜的數據聚合統計,是Java函數式編程的典型實踐。