MongoDB中消息已读未读状态怎么做_时间戳水位线与例外列表
应使用水位线+时间戳方案替代字符串状态字段:status(终态)+ statusUpdatedAt(毫秒时间戳),配合 chat_read_watermarks 独立集合与 chat_message_exceptions 例外表,通过 {receiverId:1,timestamp:-1} 复合索引和 $lt 查询提升性能。用 status 字段做已读/未读标记,但别只存字符串直接在 Message 文档里加 status: "read" 或 "unread" 最简单,但线上出问题时你会发现:状态翻转不幂等、批量标记漏数据、用户撤回消息后状态错乱。根本原因是没区分「状态归属」和「状态生效时间」。正确做法是把状态拆成两个字段:status(当前终态) + statusUpdatedAt(long 类型时间戳)。比如用户 A 给 B 发了 10 条消息,B 在 16:02:33 点开会话,此时不是遍历更新 10 条文档的 status,而是写一条「水位线」记录:{"userId": "B", "chatWith": "A", "lastReadAt": 1741708953000},后续查未读数时用 timestamp < lastReadAt 过滤,再排除掉「例外消息」(比如被撤回、被禁言期间发的)。避免用字符串比较做状态判断(如 status == "read"),MongoDB 的字符串索引效率低于数值索引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智能对话系统打造的智能语音助手解决方案
