断点续播需用userId+videoId唯一复合索引并执行findOneAndUpdate原子操作,进度字段保留两位小数,严格分离播放进度与视频元数据集合。

断点续播不是存个时间戳就完事——MongoDB 里必须兼顾写入高频、查询低延迟、索引唯一性,以及和播放器 currentTime 的毫秒级对齐。直接用 updateOne 覆盖旧记录会丢进度,不建复合唯一索引会导致重复插入失败或脏数据。
用 userId + videoId 建唯一复合索引
这是防止同一用户对同一视频产生多条进度记录的底线。没这个索引,upsert: true 可能因并发写入生成冗余文档;有但没设 {unique: true},则无法阻止重复插入。
-
db.user_video_play_process.createIndex({userId: 1, videoId: 1}, {unique: true})必须执行,且建议在集合初始化时就建好 - 字段类型要统一:
userId推荐用 ObjectId 或字符串(避免混用 number/string),videoId同理,否则索引可能失效 - 别用
_id当复合键——它无法表达业务语义,且不利于后续分片(如果未来扩容)
用 findOneAndUpdate 替代 insertOne + updateOne
播放器每 5–10 秒上报一次进度,高并发下竞态风险极高。insertOne 先查再插会漏掉中间更新,而 findOneAndUpdate 原子操作能确保“查+改”一步到位。
- 示例操作:
db.user_video_play_process.findOneAndUpdate({userId: "u123", videoId: "v456"}, {$set: {position: 128.45, updatedAt: new Date()}}, {upsert: true, returnDocument: "after"}) - 务必加
returnDocument: "after",前端需要确认最终写入值是否与预期一致(比如防止浮点精度丢失导致拖拽跳帧) - 不要依赖默认的
_id生成时间做排序——用显式updatedAt字段,方便按最后活跃时间清理过期数据
进度字段用 number 类型,单位为秒,保留小数点后两位
HTML5 播放器的 currentTime 是浮点数(如 128.45399999999998),直接存全精度会导致索引膨胀、比较失准,且无业务意义。
- 后端接收时做
Math.round(currentTime * 100) / 100截断,存成128.45 - 避免用字符串存时间(如
"2:08.45"),增加查询和计算成本 - 如果业务要求毫秒级精度(如教育类课程标记题干时间点),才升到三位小数,但需评估索引大小增长
别把播放进度和元数据混在一个集合
用户播放行为是高频写、低频读(仅续播时读一次);视频信息是低频写、高频读(封面、标题、时长等)。混存会导致集合膨胀、查询慢、备份压力大。
- 严格分离:进度存
user_video_play_process,视频元数据存videos - 进度集合禁用 TTL 索引——看似省空间,但用户清缓存或重装 App 后,
updatedAt时间戳可能被误删,导致“续播变从头开始” - 真要清理,用定时任务按
updatedAt批量删除 180 天未活跃的记录,留白给运营策略(比如 VIP 用户保留更久)
真正难的不是存进度,而是当用户在多个设备上切来切去时,如何让最后一次上报的 position 真正生效——这取决于你用的是最终一致性(推荐)还是强一致性(需牺牲写入吞吐),而 MongoDB 默认就是前者。别指望靠单次 findOneAndUpdate 解决所有同步问题,得配合客户端防抖、服务端版本号或时间戳校验。











