# 视频播放计数设计方案(Redis + 定时刷库) > 日期:2026-06-26 ## 一、设计背景 视频播放次数的统计需要避免"每播放一次就写一次数据库"的笨重做法,减少数据库写入压力。项目已集成 Redis,且提供了 `RedisUtil` 工具类和 `@Scheduled` 定时任务支持,采用 **Redis 计数器 + 定时批量刷库** 方案。 ## 二、技术选型 | 组件 | 用途 | 依据 | |------|------|------| | **RedisUtil** | 播放计数自增(内存级操作,毫秒响应) | 项目已集成 Redis,`RedisUtil` 位于 `jeecg-boot-common` 依赖 | | **@Scheduled** | 定时将 Redis 增量刷入数据库 | 项目已有 `UserInfoJob`、`RedisServiceImpl` 等 `@Scheduled` 使用先例 | | **MySQL/DM8** | 持久化存储最终播放次数 | `SppbVideo.playCount` 字段已存在 | ## 三、整体架构 ``` 用户播放视频 │ ▼ PUT /spbb/sppbVideo/{videoId}/play │ ▼ SppbVideoServiceImpl.recordPlay(videoId) │ ├── redisUtil.get("spbb:play:count:{videoId}") 读取当前计数 └── redisUtil.set("spbb:play:count:{videoId}", +1) 写入新计数 │ 操作耗时 ≈ 1ms,不涉及数据库写入 │ ┌──────────────────────────────────────────────┐ │ @Scheduled(fixedRate = 300000) │ 每5分钟 │ SppbVideoPlayFlushJob │ │ │ │ 1. redisUtil.scan("spbb:play:count:*") │ 扫描所有计数key │ 2. 遍历每个 key,解析 videoId │ │ 3. UPDATE sppb_video SET play_count += ? │ 批量更新数据库 │ 4. redisUtil.del(key) │ 清理已落库的key └──────────────────────────────────────────────┘ ``` ## 四、涉及的文件 ### 4.1 新增文件 | 文件 | 说明 | |------|------| | `jeecg-boot/.../spbb/video/job/SppbVideoPlayFlushJob.java` | 定时刷库任务,每 5 分钟执行一次,将 Redis 中的播放计数增量批量更新到 `sppb_video.play_count` 字段 | ### 4.2 修改文件 | 文件 | 操作 | 原因 | |------|------|------| | `jeecg-boot/.../spbb/video/service/ISppbVideoService.java` | 新增 `recordPlay(String videoId)` 方法声明 | Service 接口层暴露播放计数接口 | | `jeecg-boot/.../spbb/video/service/impl/SppbVideoServiceImpl.java` | 新增 `RedisUtil` 注入 + `recordPlay` 方法实现 | 使用 Redis 计数器自增播放次数 | | `jeecg-boot/.../spbb/video/controller/SppbVideoController.java` | 新增 `PUT /{videoId}/play` 接口 | 对外提供播放计数 API | | `jeecg-boot-base-core/.../shiro/ShiroConfig.java` | 新增 `/spbb/sppbVideo/*/play` 至 anon 白名单 | 外部系统免认证调用 | ## 五、详细实现 ### 5.1 播放计数接口 ```java // Controller 层 - 接收播放请求 @PutMapping(value = "/{videoId}/play") public Result recordPlay(@PathVariable String videoId) { sppbVideoService.recordPlay(videoId); return Result.OK("ok"); } // Service 层 - Redis 计数器自增(毫秒级,不写数据库) @Override public void recordPlay(String videoId) { String key = "spbb:play:count:" + videoId; Integer count = oConvertUtils.getInt(redisUtil.get(key), 0); redisUtil.set(key, count + 1); } ``` ### 5.2 定时刷库任务 ```java @Component @Slf4j public class SppbVideoPlayFlushJob { @Scheduled(fixedRate = 300000) // 每5分钟 public void flushPlayCount() { // 1. 扫描所有播放计数 key Collection keys = redisUtil.scan("spbb:play:count:*"); // 2. 逐 key 处理:解析 videoId → 获取增量 → 更新 DB → 删除 key for (String key : keys) { String videoId = key.substring("spbb:play:count:".length()); Integer increment = oConvertUtils.getInt(redisUtil.get(key), 0); SppbVideo video = sppbVideoService.getById(videoId); if (video != null) { video.setPlayCount(video.getPlayCount() == null ? increment : video.getPlayCount() + increment); sppbVideoService.updateById(video); } redisUtil.del(key); } } } ``` ## 六、关键设计决策 | 决策 | 选择 | 理由 | |------|------|------| | 计数存储 | Redis String(非 List/Set) | 单 key 单值,读写简单,`scan` 可批量扫描 | | 原子性 | 非原子自增(read + set) | `RedisUtil` 无 `incr` 方法,播放次数非关键数据,少量并发误差可接受 | | 刷库频率 | 5 分钟 | 平衡实时性与数据库压力;可在配置中调整 | | 刷库后处理 | 删除 key | 避免 key 无限堆积,下一周期重新计数 | | 服务重启恢复 | Redis RDB/AOF 持久化 | 计数不丢失;若丢失也仅是少量播放次数,影响极小 | ## 七、调用方式 ``` PUT /jeecg-boot/spbb/sppbVideo/{videoId}/play ``` - 无需认证(已加入 Shiro anon 白名单) - 无请求体 - 响应:`{"success": true, "code": 200, "message": "ok"}` - 前端可在视频播放结束时调用此接口