应使用水位线时间戳方案替代字符串状态字段status终态 statusUpdatedAt毫秒时间戳配合 chat_read_watermarks 独立集合与 chat_message_exceptions 例外表通过 {receiverId:1,timestamp:-1} 复合索引和 $lt 查询提升性能。用 status 字段做已读/未读标记但别只存字符串直接在 Message 文档里加 status: read 或 unread 最简单但线上出问题时你会发现状态翻转不幂等、批量标记漏数据、用户撤回消息后状态错乱。根本原因是没区分「状态归属」和「状态生效时间」。正确做法是把状态拆成两个字段status当前终态 statusUpdatedAtlong 类型时间戳。比如用户 A 给 B 发了 10 条消息B 在 16:02:33 点开会话此时不是遍历更新 10 条文档的 status而是写一条「水位线」记录{userId: B, chatWith: A, lastReadAt: 1741708953000}后续查未读数时用 timestamp lastReadAt 过滤再排除掉「例外消息」比如被撤回、被禁言期间发的。避免用字符串比较做状态判断如 status readMongoDB 的字符串索引效率低于数值索引lastReadAt 必须用毫秒级 long别用 Date 对象——驱动序列化可能引入时区偏差水位线记录要单独建集合如 chat_read_watermarks不要塞进 messages 集合里否则每次查未读数都要全表扫描find() 查未读数时为什么 $lt 比 $ne 快得多有人写 { status: { $ne: read } } 查未读消息初看没错但一上量就卡MongoDB 无法对字符串枚举字段高效走索引尤其当 status 还有 sending、failed、revoked 多种值时$ne 会退化为全索引扫描。而基于水位线的查询是 { senderId: A, receiverId: B, timestamp: { $lt: 1741708953000 } }只要在 { receiverId: 1, timestamp: -1 } 上建复合索引就能用上索引范围扫描QPS 提升 5–10 倍。索引字段顺序很重要receiverId 在前用于等值过滤timestamp 在后用于范围裁剪别忘了加 hint 强制走这个索引尤其在 MongoDB 4.0 多文档事务场景下优化器有时会选错执行计划如果支持「已读回执」水位线得按设备维度存如加 deviceId 字段不然 iOS 和 Android 同时在线时状态会互相覆盖撤回、禁言、定时消息怎么进「例外列表」水位线解决的是「时间切片」问题但业务规则会打破时间连续性。比如用户 B 在 16:02:33 看完消息但 16:02:45 收到一条撤回通知——这条原消息不该算已读又比如群聊中某人被禁言后发的消息即使时间戳在水位线之前也不该计入未读。 腾讯小微 基于微信AI智能对话系统打造的智能语音助手解决方案