高频计数器必须采用“分桶写入+异步聚合”模型,因单文档$inc在分片集群中仍会因路由校验、wiredtiger页锁及哈希分片导致热点写入而崩溃;分桶数建议16–64,分片键应设为{"contentid":1,"bucket":1}或混合方式,禁用纯时间或低基数字段。

直接结论:高频计数器不能靠单文档 $inc + 分片硬扛,必须拆解为“分桶写入 + 异步聚合”模型;否则无论分片键怎么选,都会在万级 QPS 下触发写冲突、Jumbo Chunk 和主从延迟。
为什么单文档 $inc 在分片集群里依然会崩
很多人以为只要对计数器字段用 $inc,再配上哈希分片键(如 {"contentId": "hashed"}),就能高并发安全更新。实际不是:
-
$inc确实是单文档原子操作,但分片环境下,每次更新仍需 mongos 路由 + config server 元数据校验 + 目标 shard 的 WiredTiger 写锁,三者叠加后吞吐上限远低于预期 - 当多个请求命中同一
contentId(比如热点文章),即使用了哈希分片,contentId值固定 → hash 结果固定 → 所有写入打到同一个 chunk → 最终落在同一 shard 上,形成事实上的“单点写瓶颈” - WiredTiger 对单文档的更新会加 page-level 锁,高并发下容易出现
WriteConflict错误,驱动层重试又放大压力 - 如果计数器还参与聚合(如
$sum统计总点赞),跨分片aggregate会触发大量网络 shuffle,延迟飙升
正确做法:用“分桶 + 异步落盘”替代单点计数器
把一个逻辑计数器(如 likeCount)拆成 N 个物理子计数器(likeCount_0, likeCount_1, ..., likeCount_{N-1}),写入时随机或按规则选择桶,读取时汇总 —— 这才是可扩展的解法。
- 分桶数量建议设为 16–64,太少起不到分流作用,太多增加聚合开销;冷热内容可动态调整(头部内容用 64 桶,长尾用 8 桶)
- 写入时用应用层哈希(如
hash(userId + contentId) % bucketCount)决定写哪个桶,避免依赖 MongoDB 的哈希分片逻辑 - 每个桶文档结构示例:
{ "_id": { "contentId": "abc123", "bucket": 7 }, "count": 1245, "updatedAt": ISODate(...) } - 分片键必须设为
{"contentId": 1, "bucket": 1}(范围分片)或{"contentId": "hashed", "bucket": 1}(混合),确保同一 contentId 的所有桶均匀散列,且支持按 contentId 高效查询全部桶 - 异步任务(如每 30 秒)扫描所有桶,将增量合并写入主计数器文档(或宽表快照),并清空桶的
count字段(留_id防丢失)
分片键设计必须避开这 3 个坑
高频计数器场景下,分片键选错等于白做分桶:
- 别用纯时间字段(如
createdAt)—— 新增计数器全挤在最新 chunk,立刻产生写热点 - 别用单一低基数字段(如
status或region)—— 关联基数 - 别只用
{"contentId": "hashed"}而忽略bucket字段 —— 哈希后contentId固定,所有桶还是落到同一分片,分桶失效 - 推荐组合:
{"contentId": "hashed", "bucket": 1}(兼顾打散与范围查),建索引时必须包含bucket,否则find({contentId: "x"})会扫全分片
Go 驱动中写分桶计数器的关键细节
用 mongo-go-driver 实现分桶写入时,最容易漏掉的是错误处理和批量策略:
- 不要为每个桶单独发
UpdateOne,改用bulk.WriteMany批量提交(即使只写一个桶,也封装成 bulk)—— 减少 round-trip 和 mongos 解析开销 - 必须设置
writeConcern: {w: 1, j: false},高频写入下开启 journal 会拖垮吞吐;丢几秒数据可接受,但不能卡住整个流程 - 捕获
mongo.WriteException中的WriteConcernError和WriteError,对WriteConflict类错误主动退避重试(指数退避,最多 3 次),而非让驱动静默失败 - 更新语句必须用
bson.M{"$inc": bson.M{"count": 1}},别传bson.M{"count": 1}(会覆盖整字段)
真正难的不是写对代码,而是意识到:MongoDB 的分片能力不等于“自动承载任意写负载”。高频计数器的本质是状态聚合问题,数据库只适合存结果,不适合扛过程 —— 把压力卸给内存、队列和定时任务,才是生产环境里稳得住的路。











