排行榜文档频繁更新拖慢写入,因每条记录单文档设计导致高并发下wiredtiger写锁竞争与索引写放大;需改用时间分片+预聚合生成准实时快照,配合批量写入、复合索引、capped/ttl控制生命周期及redis缓存降级。

为什么排行榜文档频繁更新会拖慢写入
排行榜类数据(比如用户积分榜、商品销量榜)如果按“每条记录一个文档”设计,每次排名变化都要 updateOne 修改单个文档的分数字段。当并发高、更新密时,容易触发 WiredTiger 引擎的写锁竞争,尤其在 _id 索引 + 分数索引组合下,写放大明显。更麻烦的是,$inc 虽原子,但无法解决“分数变、排名变、需重排”的本质问题——你不是在改分数,是在维护一个全局有序视图。
用时间分片 + 预聚合替代实时更新
把“实时榜单”换成“准实时快照”,是缓解压力最直接的方式。核心思路:不存“当前排名”,而存“某时间段内聚合结果”。比如按小时切片:
- 集合名用
t_rank_hourly,文档结构含hour: "2026-07-22T00:00:00Z"、userId、score、rank - 写入走
bulkWrite批量插入/替换,避免单条updateOne;每小时跑一次聚合任务,用$group+$sort+$addFields(配合$rank窗口函数)生成新快照 - 查询时只查最新一小时的集合,加
{score: 1}复合索引({hour: 1, score: -1}),跳过排序阶段
用 capped collection + TTL 控制数据生命周期
如果业务允许丢弃旧数据(比如只看最近7天榜单),不要手动 deleteMany,而是建 capped 集合或启用 TTL:
- capped 集合天然支持 FIFO,插入自动淘汰老数据,但不支持
update—— 适合“只追加、不修改”的计分流水,再靠定时聚合生成榜单 - TTL 更灵活:
db.t_rank_daily.createIndex({createdAt: 1}, {expireAfterSeconds: 604800}),注意createdAt必须是 Date 类型且有值,否则 TTL 不触发 - 两者都避免了 delete 操作对 journal 和 cache 的冲击,尤其在 WiredTiger 引擎下,delete 会产生大量 tombstone 记录
客户端缓存 + 服务端降级策略
真正卡住系统的往往不是 MongoDB,而是高频拉取同一榜单的请求。别让每个请求都穿透到 DB:
- 应用层用 Redis 缓存榜单 JSON,设置 30 秒过期 + 懒更新;缓存失效时用
find().limit(100).sort({score: -1})查,而非全量扫描 - 降级开关要能关闭聚合逻辑,直接返回上一版快照,防止聚合任务失败导致整个榜单不可用
- 警惕
skip/limit深分页:查第 1000 页榜单时,skip(99900)仍要遍历前 99900 条,必须用游标(find({score: {$lt: lastScore}}).limit(100))替代
最关键的不是“怎么存”,而是“谁来负责排序”。MongoDB 不是内存数据库,硬扛实时重排必然吃力。把排序逻辑移到聚合阶段、把更新压力转成批量写入、把查询压力挡在缓存之外——这三步漏掉任何一步,文档都会变成热点瓶颈。











