MongoDB通知Schema无界数组扩展性问题如何优化重构
问题根因
你当前的Schema属于典型的MongoDB无界数组反模式:单条通知文档的receivers和readBy数组会随着收件人规模线性膨胀,等你发全量通知时单文档数组就要存35万个ID,不仅很快会触碰到MongoDB单文档16MB的硬大小限制,数组更新、查询的索引开销也会随着数组长度指数级上升,这就是Atlas性能顾问直接报警的核心原因。
分场景建模方案
不要用单一模型覆盖所有通知场景,按通知覆盖的用户规模拆分方案,能在实现成本和性能之间拿到最好的平衡。
小范围定向通知(单聊、小群@、管理员点对点通知,收件人<1000)
这类场景不需要完全推翻原有设计,只要把无限增长的已读数组合拆出去即可:
- 保留Notification主文档的
receivers字段,但是在业务代码层加硬校验,数组长度超过1000就自动走广播通知逻辑,禁止写入超长数组 - 彻底删除Notification文档内嵌的
readBy数组,单独新建NotificationReadStatus集合存储已读状态,Schema如下:
{ notificationId: { type: Schema.Types.ObjectId, ref: 'Notification', required: true }, userId: { type: Schema.Types.ObjectId, ref: 'User', required: true }, readAt: { type: Date, default: Date.now } } // 必须建复合唯一索引,避免重复写入已读状态,同时加速查询 db.notification_read_status.createIndex({ notificationId: 1, userId: 1 }, { unique: true }) // 按用户查已读记录的索引 db.notification_read_status.createIndex({ userId: 1, notificationId: 1 })
- 查用户未读通知时,只需要找收件人包含当前用户、且在
notification_read_status中没有对应记录的通知即可,不需要扫描大数组。
大范围/全量广播通知(全站公告、全体已验证用户推送,收件人>1000)
这类场景绝对不能给每个收件人生成关联记录——35万用户发一条全量通知就要写35万条关联数据,写流量会直接打满数据库,用「范围标记+拉取时匹配」的模式实现:
- 扩展Notification主文档结构,去掉无长度限制的receivers数组,新增通知类型和受众筛选字段:
{ message: { type: String, required: true }, sender: { type: Schema.Types.ObjectId, ref: 'User' }, // 通知类型枚举 type: { type: String, enum: ['targeted', 'broadcast'], required: true, index: true }, // 仅定向通知存收件人ID,加长度校验 receivers: { type: [{ type: Schema.Types.ObjectId, ref: 'User' }], validate: v => !v || v.length <= 1000 }, // 仅广播通知存受众匹配规则,比如给所有已验证用户发通知就存 { verified: true } audienceRule: { type: Object, default: null }, createdAt: { type: Date, default: Date.now, index: true } }
- 已读状态完全复用上面的
notification_read_status集合,不需要额外改结构 - 拉取用户通知列表时,合并两类结果即可:
- 定向通知:
type: 'targeted'且receivers包含当前用户ID - 广播通知:
type: 'broadcast'且audienceRule匹配当前用户属性(比如用户已验证就匹配audienceRule.verified = true的通知) - 合并结果后关联已读状态表,标记哪些通知已读,按
createdAt倒序做游标分页返回。
- 定向通知:
关键性能注意事项
- 不要给广播通知预生成用户收件记录,这是订阅类通知模型最常见的性能坑:不仅写放大极其严重,还会浪费数倍的存储空间。
- 通知列表必须用
createdAt+_id做游标分页,不要用skip+limit,数据量超过10万之后翻页性能差距可达两个数量级。 - 做冷数据归档:默认只给用户返回近3个月的通知,更早的历史通知迁移到归档集合,降低热集合的数据量。
- 在User文档上冗余
unreadCount字段,发新通知、用户标记已读时用原子操作增减计数,不要每次实时count未读记录,接口响应速度能提升10倍以上。
内容的提问来源于stack exchange,提问作者Eazy
相关产品推荐
相关产品推荐

