视频播放计数设计方案(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 播放计数接口
// Controller 层 - 接收播放请求
@PutMapping(value = "/{videoId}/play")
public Result<String> 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 定时刷库任务
@Component
@Slf4j
public class SppbVideoPlayFlushJob {
@Scheduled(fixedRate = 300000) // 每5分钟
public void flushPlayCount() {
// 1. 扫描所有播放计数 key
Collection<String> 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"}
- 前端可在视频播放结束时调用此接口