【Easylive】定时任务-每日数据统计和临时文件清理

【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. 关键技术点
  1. Redis+DB混合统计
    • 播放量等高频数据先记录到Redis
    • 定时任务从Redis获取昨日数据后持久化到DB

  2. 多维度统计
    • 播放量:基于视频ID聚合后关联用户
    • 粉丝量:直接查询DB关系数据
    • 互动数据:统一处理点赞/收藏/投币等行为

  3. 批量操作优化
    • 使用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. 设计要点
  1. 安全机制
    • 文件名强制使用日期格式(如20230815
    • 只删除命名合规的文件夹

  2. 容错处理
    • 捕获IOException防止单次失败影响后续操作
    • 空文件夹自动跳过

  3. 性能考虑
    • 高频检查(每分钟)但低负载(仅处理过期文件)
    • 使用FileUtils工具类保证删除可靠性


三、架构设计亮点

  1. 解耦设计
    • 统计服务与业务逻辑分离
    • 文件清理与业务模块隔离

  2. 数据一致性

    业务操作
    Redis
    定时统计任务
    DB

    • 实时数据写入Redis保证性能
    • 定时同步到DB保证持久化

  3. 扩展性
    • 新增统计维度只需添加对应方法
    • 文件清理策略可配置化


四、潜在优化建议

  1. 统计任务优化

    // 可考虑分片统计(大用户量场景)
    @Scheduled(cron = "0 0 1 * * ?") 
    public void statsUserShard1() {statisticsService.processByUserRange(0, 10000);
    }
    
  2. 文件清理增强

    // 添加文件大小监控
    if (file.length() > MAX_TEMP_FILE_SIZE) {alertService.notifyOversizeFile(file);
    }
    
  3. 异常处理强化

    @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(视频信息列表,包含videoIduserId等字段)
目标:生成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
}

执行过程:

  1. 流处理开始
  2. 遇到v1(用户u1)→ 计算播放量100 → u1分组累计100
  3. 遇到v2(用户u1)→ 计算播放量50 → u1分组累计150
  4. 遇到v3(用户u2)→ 计算播放量200 → u2分组累计200

最终结果:

{u1=150, u2=200}

五、设计优势

  1. 高效聚合
    单次遍历完成分组+求和,时间复杂度O(n)

  2. 内存友好
    流式处理避免中间集合的创建

  3. 可维护性
    清晰表达业务逻辑:
    “按用户分组,求和每个用户所有视频的播放量”

  4. 扩展性
    如需修改统计逻辑(如求平均值),只需替换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函数式编程的典型实践。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/76531.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

蓝桥杯 15g

班级活动 问题描述 小明的老师准备组织一次班级活动。班上一共有 nn 名 (nn 为偶数) 同学&#xff0c;老师想把所有的同学进行分组&#xff0c;每两名同学一组。为了公平&#xff0c;老师给每名同学随机分配了一个 nn 以内的正整数作为 idid&#xff0c;第 ii 名同学的 idid 为…

如何使用AI辅助开发R语言

R语言是一种用于统计计算和图形生成的编程语言和软件环境&#xff0c;很多学术研究和数据分析的科学家和统计学家更青睐于它。但对与没有编程基础的初学者而言&#xff0c;R语言也是有一定使用难度的。不过现在有了通义灵码辅助编写R语言代码&#xff0c;我们完全可以用自然语言…

CISCO组建RIP V2路由网络

1.实验准备&#xff1a; 2.具体配置&#xff1a; 2.1根据分配好的IP地址配置静态IP&#xff1a; 2.1.1PC配置&#xff1a; PC0&#xff1a; PC1&#xff1a; PC2&#xff1a; 2.1.2路由器配置&#xff1a; R0&#xff1a; Router>en Router#conf t Enter configuration…

React + TipTap 富文本编辑器 实现消息列表展示,类似Slack,Deepseek等对话框功能

经过几天折腾再折腾&#xff0c;弄出来了&#xff0c;弄出来了&#xff01;&#xff01;&#xff01; 消息展示 在位编辑功能。 两个tiptap实例1个用来展示 消息列表&#xff0c;一个用来在位编辑消息。 tiptap灵活富文本编辑器&#xff0c;拓展性太好了!!! !!! 关键点&#x…

Ubuntu搭建Pytorch环境

Ubuntu搭建Pytorch环境 例如&#xff1a;第一章 Python 机器学习入门之pandas的使用 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 Ubuntu搭建Pytorch环境前言一、Anaconda二、Cuda1.安装流程2、环境变量&#…

Sping Cloud配置和注册中心

1.Nacos实现原理了解吗&#xff1f; Nacos是注册中心&#xff0c;主要是帮助我们管理服务列表。Nacos的实现原理大概可以从下面三个方面来讲&#xff1a; 服务注册与发现&#xff1a;当一个服务实例启动时&#xff0c;它会向Nacos Server发送注册请求&#xff0c;将自己的信息…

C++笔记之父类引用是否可以访问到子类特有的属性?

C++笔记之父类引用是否可以访问到子类特有的属性? code review! 参考笔记 1.C++笔记之在基类和派生类之间进行类型转换的所有方法 文章目录 C++笔记之父类引用是否可以访问到子类特有的属性?1.主要原因2.示例代码3.说明4.如何访问子类特有的属性5.注意事项6.总结在 C++ 中,…

JavaScript逆向工程:如何判断对称加密与非对称加密

在现代Web应用安全分析中&#xff0c;加密算法的识别是JavaScript逆向工程的关键环节。本文将详细介绍如何在逆向工程中判断JavaScript代码使用的是对称加密还是非对称加密。 一、加密算法基础概念 1. 对称加密 (Symmetric Encryption) 特点&#xff1a;加密和解密使用相同的…

物理备份工具 BRM vs gs_probackup

什么是BRM 上一篇文章讲了openGauss的物理备份工具gs_probackup&#xff0c;今天来说说BRM备份工具。 BRM备份恢复工具全称为&#xff1a;Backup and Recovery Manager&#xff0c;是MogDB基于opengauss的备份工具 gs_probackup 做了一些封装和优化,面向MogDB数据库实现备份和…

问问lua怎么写DeepSeek,,,,,

很坦白说&#xff0c;这十年&#xff0c;我几乎没办法从互联网找到这个这样的代码&#xff0c;互联网引擎找不到&#xff0c;我也没有很大的“追求”要传承&#xff0c;或者要宣传什么&#xff1b;直到DeepSeek的出现 兄弟&#xff0c;Deepseek现在已经比你更了解你楼下的超市…

react+Tesseract.js实现前端拍照获取/选择文件等文字识别OCR

需求背景 在开发过程中可能会存在用户上传一张图片后下方需要自己识别出来文字数字等信息&#xff0c;有的时候会通过后端来识别后返回&#xff0c;但是也会存在纯前端去识别的情况&#xff0c;这个时候就需要使用到Tesseract.js这个库了 附Tesseract.js官方&#xff08;htt…

蓝桥杯考前复盘

明天就是考试了&#xff0c;适当的停下刷题的步伐。 静静回望、思考、总结一下&#xff0c;我走过的步伐。 考试不是结束&#xff0c;他只是检测这一段时间学习成果的工具。 该继续走的路&#xff0c;还是要继续走的。 只是最近&#xff0c;我偶尔会感到迷惘&#xff0c;看…

前端-Vue3

1. Vue3简介 2020年9月18日&#xff0c;Vue.js发布版3.0版本&#xff0c;代号&#xff1a;One Piece&#xff08;n 经历了&#xff1a;4800次提交、40个RFC、600次PR、300贡献者 官方发版地址&#xff1a;Release v3.0.0 One Piece vuejs/core 截止2023年10月&#xff0c;最…

[ctfshow web入门] web39

信息收集 题目发生了微妙的变化&#xff0c;只过滤flag&#xff0c;include后固定跟上了.php。且没有了echo $flag;&#xff0c;虽说本来就没什么用 if(isset($_GET[c])){$c $_GET[c];if(!preg_match("/flag/i", $c)){include($c.".php");} }else{…

【动手学深度学习】LeNet:卷积神经网络的开山之作

【动手学深度学习】LeNet&#xff1a;卷积神经网络的开山之作 1&#xff0c;LeNet卷积神经网络简介2&#xff0c;Fashion-MNIST图像分类数据集3&#xff0c;LeNet总体架构4&#xff0c;LeNet代码实现4.1&#xff0c;定义LeNet模型4.2&#xff0c;定义模型评估函数4.3&#xff0…

代码随想录第15天:(二叉树)

一、二叉搜索树的最小绝对差&#xff08;Leetcode 530&#xff09; 思路1 &#xff1a;中序遍历将二叉树转化为有序数组&#xff0c;然后暴力求解。 class Solution:def __init__(self):# 初始化一个空的列表&#xff0c;用于保存树的节点值self.vec []def traversal(self, r…

计算机操作系统-【死锁】

文章目录 一、什么是死锁&#xff1f;死锁产生的原因&#xff1f;死锁产生的必要条件&#xff1f;互斥条件请求并保持不可剥夺环路等待 二、处理死锁的基本方法死锁的预防摒弃请求和保持条件摒弃不可剥夺条件摒弃环路等待条件 死锁的避免银行家算法案例 提示&#xff1a;以下是…

vue拓扑图组件

vue拓扑图组件 介绍技术栈功能特性快速开始安装依赖开发调试构建部署 使用示例演示截图组件源码 介绍 一个基于 Vue3 的拓扑图组件&#xff0c;具有以下特点&#xff1a; 1.基于 vue-flow 实现&#xff0c;提供流畅的拓扑图展示体验 2.支持传入 JSON 对象自动生成拓扑结构 3.自…

go 通过汇编分析函数传参与返回值机制

文章目录 概要一、前置知识二、汇编分析2.1、示例2.2、汇编2.2.1、 寄存器传值的汇编2.2.2、 栈内存传值的汇编 三、拓展3.1 了解go中的Duff’s Device3.2 go tool compile3.2 call 0x46dc70 & call 0x46dfda 概要 在上一篇文章中&#xff0c;我们研究了go函数调用时的栈布…

python-1. 找单独的数

问题描述 在一个班级中&#xff0c;每位同学都拿到了一张卡片&#xff0c;上面有一个整数。有趣的是&#xff0c;除了一个数字之外&#xff0c;所有的数字都恰好出现了两次。现在需要你帮助班长小C快速找到那个拿了独特数字卡片的同学手上的数字是什么。 要求&#xff1a; 设…