不该按天建独立集合。手动命名如messages_20240520会导致路由、备份、聚合、跨日查询混乱,ORM易错;TTL索引+合理分片已可支撑亿级消息。

消息集合该不该按天建独立集合?
不该。手动用 messages_20240520 这类名字分集合,表面看能控制单集合大小,实际会让路由、备份、聚合、跨日查询全乱套。驱动不支持动态集合名批量操作,ORM 映射极易出错;TTL 索引 + 合理分片已足够扛住亿级消息,没必要自己造轮子。
实操建议:
• 用单集合 messages,靠 created_at 字段建 TTL 索引自动过期
• 若读写集中在最近 7 天,可设 {created_at: 1} 为 shard key,但必须确保写入时间单调递增,否则 chunk 拆分不均
• 绝对不要拼接日期字符串生成集合名
为什么 skip/limit 在聊天场景下会漏数据或游标失效?
因为高并发写入下,skip(20).limit(10) 本质是“跳过前 20 条再取 10 条”,但第 21 条可能刚被插入,导致偏移错位;而基于 _id 或 created_at 的游标分页,若没处理好边界条件(比如重复时间戳、时钟不同步),也会跳过或重复某条。
实操建议:
• 第一页用 find().sort({created_at: -1, _id: -1}).limit(20)
• 后续页传上一页最后一条的 created_at 和 _id,查:db.messages.find({created_at: {$lt: lastMsg.created_at}, _id: {$lt: lastMsg._id}}).sort({created_at: -1, _id: -1}).limit(20)
• 必须建复合索引:db.messages.createIndex({created_at: -1, _id: -1}),字段顺序不能颠倒
• 避免用 $gte+$lt 查“某天全部消息”再分页——这是全量扫描,响应随数据增长线性变慢
如何让“加载更多”既快又准?
关键不是查得快,而是每次请求返回的文档尽可能少走磁盘、少排序、不阻塞写入。默认 sort + limit 在大 offset 下性能断崖下跌,尤其当 created_at 有大量重复值(毫秒级时间戳+高并发)时。
实操建议:
• 把 _id 当隐式时间戳用:ObjectId 前 4 字节是秒级时间戳,升序即时间顺序,天然去重
• 查询时强制 projection 只取必要字段,例如:{content: 1, sender_id: 1, created_at: 1, _id: 1},避免传输和序列化整条消息
• 客户端缓存上一页的 last_id 和 last_created_at,服务端校验二者是否一致——防止因时钟不同步或写入延迟导致游标漂移
• 不要依赖 count() 判断是否到底,直接看本次返回条数是否等于 limit:等于就继续拉,小于就停
会话与消息之间用嵌入还是引用?
嵌入只适合极短生命周期、极低更新频率的会话元信息(比如会话标题、创建时间),消息本身必须独立存在。消息是高频写入、按时间范围查询、需 TTL 清理、常跨会话聚合的实体,硬塞进会话文档会导致文档膨胀、更新锁竞争、索引失效。
实操建议:
• 会话集合 sessions 存基础字段:_id、user_ids、title、updated_at
• 消息集合 messages 存核心字段:session_id(索引)、sender_id、content、created_at、_id
• 在 messages 上建索引:{session_id: 1, created_at: -1, _id: -1},支撑按会话+时间倒序高效分页
• 不要在 sessions 里嵌入消息数组,也不用“桶模式”按天/按会话分组——这会让分页逻辑和权限管理失控
真正难的不是写对第一条游标查询,而是保证所有客户端传来的 last_id 和 last_created_at 严格匹配服务端索引顺序,且在时钟偏差、网络重试、多实例部署下依然不漂移。这点容易被忽略,但一旦出问题就是数据错乱,不是性能问题。

















