在线教育平台视频进度同步优化实践 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提升到420095%的读取延迟从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分钟)这种设计带来两个优势高频更新的String类型只用20字节左右内存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() { RDelayedQueueString queue redisson.getDelayedQueue( redisson.getScoredSortedSet(delay_queue:progress)); // 每次处理最多100条避免长事务 ListString members queue.poll(100); if (!members.isEmpty()) { // 批量查询Redis最新进度 ListString keys members.stream() .map(m - progress: m) .collect(Collectors.toList()); ListProgress progressList redisTemplate.opsForValue().multiGet(keys); // 批量更新MySQL batchUpdateToDatabase(members, progressList); } }这里有个关键细节每次从ZSET获取元素时会同时用ZREMRANGEBYSCORE删除已处理项保证不会重复消费。4. 性能优化实践4.1 热点数据处理针对热门课程视频我们增加了本地缓存层// 使用Caffeine做二级缓存 LoadingCacheString, 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 补偿机制设计我们建立了三级补偿体系定时全量同步每天凌晨扫描ZSET剩余项异常重试队列对数据库写入失败的数据进入重试队列人工修复接口提供按时间范围修复的Admin API5.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. 踩坑经验分享坑1ZSET的内存增长问题初期没有及时清理已处理项导致ZSET在高峰期每小时增长2GB。解决方案是// 在处理完成后立即清理 redisTemplate.opsForZSet().removeRangeByScore( delay_queue:progress, 0, System.currentTimeMillis());坑2Redisson的序列化兼容性发现Jackson序列化与Redisson默认编码冲突最终统一使用config.setCodec(new JsonJacksonCodec());坑3MySQL批量写入瓶颈当批量插入超过500条时出现锁等待最终采用INSERT INTO progress(user_id, video_id, time) VALUES (?,?,?), (?,?,?) ON DUPLICATE KEY UPDATE timeVALUES(time)配合100条/批次的拆分策略。