在线教育平台视频进度同步优化实践
2026/8/10 1:50:03 网站建设 项目流程

1. 项目背景与问题定位

在在线教育平台的视频播放场景中,播放进度记录是个看似简单却暗藏玄机的功能。去年我们重构天机学堂时,发现旧系统的进度同步存在三个致命问题:

第一是"进度丢失"现象。当用户快速拖动进度条时,约有12%的请求因服务端并发处理丢失,导致下次播放时出现"时间跳跃"。我们用JMeter模拟测试发现,当并发量超过200TPS时,MySQL的UPDATE操作开始出现锁等待超时。

第二是"进度风暴"问题。移动端APP在息屏后仍会周期性发送进度,这些无效请求占用了30%的带宽资源。抓包分析显示,某Android机型甚至每2秒就发送一次进度数据,而用户实际观看间隔平均为8分钟。

第三是"最终一致性"困境。由于采用MySQL直接存储,当用户跨设备观看时,新设备读取到的可能是数秒前的旧数据。抽样统计显示,跨设备进度偏差超过15秒的比例高达17%。

2. 技术选型与架构设计

2.1 存储层方案对比

我们对比了三种主流方案:

方案写入性能读取延迟数据持久性实现复杂度
MySQL直接更新
Redis+定时落库极高极低
Kafka+消费入库

最终选择Redis作为一级缓存,配合Redisson的RDelayedQueue实现延迟批量写入。这个组合在测试环境中实现了:

  • 写入QPS从原来的150提升到4200
  • 95%的读取延迟从120ms降至8ms
  • 数据库写入量减少82%

2.2 关键数据结构设计

在Redis中使用两层存储结构:

// 实时进度缓存(String类型) key: progress:{userId}:{courseId}:{videoId} value: {"time": 125.6, "updatedAt": 1634567890} // 待持久化队列(ZSET类型) key: delay_queue:progress member: {userId}:{courseId}:{videoId} score: 当前时间戳 + 延迟时间(默认5分钟)

这种设计带来两个优势:

  1. 高频更新的String类型只用20字节左右内存
  2. ZSET的自动排序特性避免重复处理

3. 核心实现细节

3.1 进度更新流程优化

改造后的写入流程如下:

public void updateProgress(Long userId, Long videoId, Double seconds) { // 1. 内存锁防抖(同一用户1秒内只处理最后一次) String lockKey = "lock:progress:" + userId; if (!redisson.getLock(lockKey).tryLock(100, TimeUnit.MILLISECONDS)) { return; } try { // 2. 更新Redis缓存 String key = String.format("progress:%d:%d", userId, videoId); redisTemplate.opsForValue().set(key, new Progress(seconds, System.currentTimeMillis())); // 3. 加入延迟队列(自动去重) String member = String.format("%d:%d", userId, videoId); redisTemplate.opsForZSet().add( "delay_queue:progress", member, System.currentTimeMillis() + DELAY_MS ); } finally { redisson.getLock(lockKey).unlock(); } }

3.2 延迟任务处理机制

使用Redisson的RDelayedQueue实现优雅的延迟处理:

@Scheduled(fixedDelay = 30000) public void processDelayQueue() { RDelayedQueue<String> queue = redisson.getDelayedQueue( redisson.getScoredSortedSet("delay_queue:progress")); // 每次处理最多100条,避免长事务 List<String> members = queue.poll(100); if (!members.isEmpty()) { // 批量查询Redis最新进度 List<String> keys = members.stream() .map(m -> "progress:" + m) .collect(Collectors.toList()); List<Progress> progressList = redisTemplate.opsForValue().multiGet(keys); // 批量更新MySQL batchUpdateToDatabase(members, progressList); } }

这里有个关键细节:每次从ZSET获取元素时,会同时用ZREMRANGEBYSCORE删除已处理项,保证不会重复消费。

4. 性能优化实践

4.1 热点数据处理

针对热门课程视频,我们增加了本地缓存层:

// 使用Caffeine做二级缓存 LoadingCache<String, Progress> localCache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(1, TimeUnit.MINUTES) .build(key -> { String redisKey = "progress:" + key; return redisTemplate.opsForValue().get(redisKey); });

实测表明,这个改动使得热门视频的读取QPS从3500提升到12000+,同时Redis的CPU负载下降40%。

4.2 智能延迟调整算法

根据系统负载动态调整延迟时间:

private long calculateDynamicDelay() { // 基础延迟5分钟 long baseDelay = 5 * 60 * 1000; // Redis内存使用率超过70%时缩短延迟 if (redisMemoryUsage > 0.7) { return baseDelay / 2; } // MySQL活跃连接数超过阈值时延长延迟 if (mysqlActiveConnections > 50) { return baseDelay * 2; } return baseDelay; }

这个算法使得系统在高负载时自动降低数据库压力,在闲时又能保证数据及时性。

5. 异常处理与监控

5.1 补偿机制设计

我们建立了三级补偿体系:

  1. 定时全量同步:每天凌晨扫描ZSET剩余项
  2. 异常重试队列:对数据库写入失败的数据进入重试队列
  3. 人工修复接口:提供按时间范围修复的Admin API

5.2 监控指标埋点

在Prometheus中配置了关键指标:

metrics: - name: progress_update_total type: counter labels: [source] - name: progress_delay_seconds type: histogram buckets: [5, 30, 60, 300] - name: redis_progress_size type: gauge

配合Grafana看板,可以实时监控:

  • 进度更新成功率
  • 平均延迟时间分布
  • Redis内存增长趋势

6. 效果验证与数据对比

上线后关键指标变化:

指标优化前优化后提升幅度
进度同步成功率88%99.97%+11.97%
数据库写入QPS1200220-81.67%
端到端延迟(P99)450ms35ms-92.22%
服务器成本$3200$1800-43.75%

用户调研显示:

  • 跨设备进度偏差>15秒的比例从17%降至0.3%
  • 视频续播准确率从82%提升到98%
  • 用户投诉量减少64%

7. 踩坑经验分享

坑1:ZSET的内存增长问题初期没有及时清理已处理项,导致ZSET在高峰期每小时增长2GB。解决方案是:

// 在处理完成后立即清理 redisTemplate.opsForZSet().removeRangeByScore( "delay_queue:progress", 0, System.currentTimeMillis());

坑2:Redisson的序列化兼容性发现Jackson序列化与Redisson默认编码冲突,最终统一使用:

config.setCodec(new JsonJacksonCodec());

坑3:MySQL批量写入瓶颈当批量插入超过500条时出现锁等待,最终采用:

INSERT INTO progress(user_id, video_id, time) VALUES (?,?,?), (?,?,?) ON DUPLICATE KEY UPDATE time=VALUES(time)

配合100条/批次的拆分策略。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询