不能依赖实时count查询,因其在1800万+文档集合上常超200ms,且未命中索引时会全量扫描拖垮集群;应改用写时更新+读时缓存的计数文档方案,按业务维度拆分、原子更新、毫秒读取,并通过校准job、TTL索引和时间桶模型保障准确与性能。

为什么不能依赖实时 count 查询
因为 count_documents({}) 在 1800 万+ 文档的集合上,哪怕加了 _id_ 索引,单次执行也常超 200ms;而带条件的 count_documents(filter) 若没命中复合索引,可能直接扫全量、CPU 拉满、拖垮 mongos。这不是配置问题,是 WiredTiger 引擎在分片集群下必须做协调扫描的硬开销。
用计数文档替代实时 count 的核心逻辑
本质是把「每次查时算」变成「写时更新 + 读时取缓存」。关键不是存一个总数,而是按业务维度拆解:用户维度、时间窗口、状态分组等,各自维护独立计数文档。
- 每张计数表结构统一:
{ _id: "user_123", count: 142, updated_at: ISODate(...) } - 写操作(insert/update/delete)必须同步更新对应计数文档,用
findAndModify或updateOne(..., { upsert: true })保证原子性 - 读请求直接
findOne({ _id: "user_123" }),毫秒级返回,不走路由、不触发协调 - 避免单点热点:不要用
{ _id: "total" }这种全局计数,高并发写会卡在单个 shard 上
如何安全处理迁移和异常场景
计数文档本身不参与分片路由,但它的更新行为受 balancer 影响——如果正在迁移目标 shard,updateOne 可能因 chunk 移动失败或重试,导致计数滞后。
- 所有写入计数文档的操作必须设
max_time_ms=5000,超时即告警,不重试 - 每日凌晨跑一次校准 job:
count_documents(filter)对比计数文档值,差值 > 0.1% 就触发修复流程 - 校准 job 必须在
sh.isBalancerRunning()返回false时执行,否则迁移中统计无意义 - 计数文档加 TTL 索引(如
updated_at: 1+expireAfterSeconds: 86400),自动清理陈旧条目
聚合类计数场景怎么落地
比如要统计「过去 7 天每小时的订单量」,别用 $group + $sum 聚合实时算——它无法利用索引加速,且结果不可缓存。
- 改用时间桶(time-bucket)模型:每小时生成一条文档,
_id为"2026-05-11T13:00:00Z",字段含order_count、refund_count等 - 写入订单时,用
updateOne({ _id: hourBucketId }, { $inc: { order_count: 1 } }, { upsert: true }) - 查询最近 24 小时数据,只需
find({ _id: { $gte: "2026-05-10T14:00:00Z" } }),6 行代码搞定 - 注意:桶 ID 必须用 UTC 时间,且严格对齐整点,否则聚合错位
真正难的不是写那几行 updateOne,而是让所有写路径(包括后台任务、消息队列消费、跨服务调用)都遵守同一套计数更新契约——漏掉任意一环,计数就不可信。











