capped集合仅适合丢弃可接受、时效强、读写极简的通知场景;因其不可删改、无二级索引、按空间/数量淘汰而非时间,一旦需按用户查询、标记已读、回溯或聚合即成技术债。

用 capped 集合做实时通知的存储底座,可行,但仅适合「丢弃可接受、时效强、读写极简」的场景;一旦需要按用户查、标记已读、回溯历史或做聚合,它立刻变成技术债源头。
为什么capped集合看起来很适合通知?
它天然支持插入即追加、自动淘汰旧文档、固定空间、无索引开销——这些和“最新 N 条推送”高度匹配。但关键限制在于:不能删、不能更新、不能建二级索引。
常见误用现象:
- 想用
updateOne({ _id: xxx }, { $set: { read: true } })标记已读 → 失败,capped集合禁止修改文档大小(哪怕只改一个布尔值) - 想按
user_id查询某人所有未读 → 只能全表扫描,且无法加{ user_id: 1 }索引,查询越来越慢 - 想保留最近 7 天通知,但按时间淘汰 →
capped只认空间/文档数,不认时间,容易把刚发的紧急通知挤掉
capped集合真正适用的通知子场景
只在以下情况才建议直接用:全局广播类、瞬时状态类、调试日志类,例如:
- 系统级告警流(如 CPU >95% 的实时快照,只看最新 1000 条)
- 设备心跳缓冲(每秒一条,只关心最后在线时间,不关心谁发的)
- 灰度开关变更日志(供运维快速翻阅,不要求追溯、不需分用户)
创建示例:
db.createCollection("alert_stream", {
capped: true,
size: 10485760, // 10MB
max: 5000 // 最多存 5000 条,任一条件触发即淘汰
})
注意:size 和 max 是「或」关系,不是「且」;生产环境建议只设 size,避免小文档撑满条数却没占多少空间。
如果必须支持用户维度+已读状态,怎么绕过capped限制?
核心思路:**用 capped 做「写入缓冲」,用普通集合做「主存储」,靠 Change Stream 或 Tail Log 同步**。
典型双写结构:
- 写入端:先写入
notification_buffer(capped),再异步写入notifications(普通集合,带{ user_id: 1, created_at: -1 }复合索引) - 读取端:永远从
notifications查,用read: true/false字段 + TTL 索引自动清理过期通知 - 缓冲作用:扛住突发推送洪峰,防止主集合写入阻塞;同时为同步逻辑提供重试窗口(比如下游服务宕机 2 分钟,buffer 还在)
这种设计下,capped 不再是主模型,而是纯基础设施层组件——它的容量、淘汰策略、是否启用,对业务逻辑零耦合。
Change Stream 同步时最容易漏掉的坑
用 MongoDB 4.0+ 的 Change Stream 从 capped 拉变更,看似简单,实则几个硬伤:
-
capped集合没有_id保证唯一性,同一文档可能被重复拉取(尤其网络抖动后 resume token 失效)→ 必须在下游做幂等写入(如用{ user_id, event_id }唯一索引) - Change Stream 默认不包含完整文档,
fullDocument: "updateLookup"在capped上无效 → 别指望能拿到更新前/后的快照,只能拿到操作类型 + 文档本身 - 没有「消费位点确认」机制,重启后只能从最新位置开始 → 若要求至少一次语义,得自己维护 offset(比如写进另一个集合)
更稳妥的做法:不用 Change Stream,改用定时轮询 notification_buffer 的 $natural 排序 + 记录最后处理的 $natural 位置(find().sort({ $natural: 1 }).limit(1) 可获取最早文档)。
真正难的从来不是「怎么存通知」,而是「怎么定义什么算一条有效通知」——是设备上报的原始事件?还是经过规则引擎过滤、富化、去重后的结果?capped 集合连字段校验都做不到,上游数据质量失控时,它只会加速问题暴露。


















