好友聊天已读状态总结 在即时通讯系统中用户需要知道发送的消息是否被对方阅读。传统方案是为每条消息单独存储 is_read 字段但这种方式在高并发场景下会导致数据库压力过大——每收到一条消息就要更新一条记录消息量增长后性能急剧下降。为解决这个问题我借鉴了一些大佬的想法基于IM系统的设计采用会话维度水位记录而不是逐条标记核心思想 - 每条消息在会话内分配单调递增序号 seq - 每个用户在每个会话只存一条阅读水位记录 - 判断规则消息.seq 用户.last_read_seq → 已读方案对比数据库设计消息表新增序号列ALTER TABLE chat_msg ADD COLUMN seq BIGINT NOT NULL DEFAULT 0 COMMENT 会话内单调递增序号;会话序号分配表CREATE TABLE conversation_seq ( conversation_key VARCHAR(50) PRIMARY KEY COMMENT 会话标识小ID_大ID, seq BIGINT NOT NULL DEFAULT 0 COMMENT 下一条消息的序号 );会话阅读水位表CREATE TABLE conversation_read ( id BIGINT PRIMARY KEY AUTO_INCREMENT, conversation_key VARCHAR(50) NOT NULL COMMENT 会话标识, user_id BIGINT NOT NULL COMMENT 阅读方用户ID, last_read_seq BIGINT NOT NULL DEFAULT 0 COMMENT 读到的最大消息序号, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_conv_user (conversation_key, user_id) );后端实现1.会话序号分配原子递增Insert(INSERT INTO conversation_seq (conversation_key, seq) VALUES (#{key}, 1) ON DUPLICATE KEY UPDATE seq seq 1) void incrementSeq(String key);每次发送消息时调用INSERT … ON DUPLICATE KEY UPDATE 保证原子性。2.阅读水位更新取最大值Insert(INSERT INTO conversation_read (conversation_key, user_id, last_read_seq) VALUES (#{key}, #{userId}, #{seq}) ON DUPLICATE KEY UPDATE last_read_seq GREATEST(last_read_seq, #{seq})) void upsertLastReadSeq(String key, Long userId, Long seq);3. 发送消息时分配序号public ChatMsg saveMsg(Long sendId, Long receiveId, String content) { String convKey buildKey(sendId, receiveId); // 小ID_大ID long seq conversationService.nextSeq(convKey); ChatMsg msg new ChatMsg(); msg.setSeq(seq); // ... 其他字段 save(msg); return msg; }4. 获取历史消息时计算已读状态public ChatHistoryVO getHistoryMsg(Long userId, Long friendId) { String convKey buildKey(userId, friendId); // 打开聊天时先更新我的阅读水位 long myLastReadSeq updateMyReadSeq(convKey, userId, friendId); // 查询对方的阅读水位判断我发的消息是否被对方已读 long friendLastReadSeq getLastReadSeq(convKey, friendId); // 查询消息列表 ListChatMsg list list(...); // 计算每条消息的已读状态 for (ChatMsg m : list) { if (m.getReceiveUserId().equals(userId)) { // 对方发给我的用我的水位判断 m.setReadStatus(m.getSeq() myLastReadSeq ? 1 : 0); } else if (m.getSendUserId().equals(userId)) { // 我发给对方的用对方的水位判断 m.setReadStatus(m.getSeq() friendLastReadSeq ? 1 : 0); } } return new ChatHistoryVO(list, myLastReadSeq); }5.WebSocket 推送已读回执PostMapping(/read) public ResultVoid readMsg(RequestParam String username) { Long userId UserHolder.getUserId(); User friend userService.selectByUserName(username); long maxSeq chatMsgService.readMsg(friend.getId(), userId); // 推送已读回执给发送方 if (maxSeq 0) { ChatWebSocket.pushToUser(friend.getId(), READ| maxSeq | userId); } return Result.success(null); }前端实现收到新消息时立即标记已读onPush: (msg) { if (String(msg.sendUserId) String(activeId.value)) { // 我在聊天界面内 → 立即标记已读 messages.value.push({ ...msg, readStatus: 1 // 本地立即标记 }); // 异步调用 readMsg更新后端水位并推送回执 readMsg(activeUsername.value).catch(() {}); } }收到已读回执时批量更新状态:onRead: (maxSeq, readerId) { // readerId 已读到 maxSeq // 我发给 readerId 的消息中 seq maxSeq 的 → 已读 messages.value.forEach((m) { if (String(m.sendUserId) String(myId.value) String(m.receiveUserId) String(readerId) m.seq maxSeq) { m.readStatus 1; } }); }关键结束点会话标识固定格式min(userId1, userId2) “_” max(userId1, userId2)保证双方使用同一 key原子递增序号INSERT … ON DUPLICATE KEY UPDATE seq seq 1水位更新防回退GREATEST(last_read_seq, #{seq}) 取最大值前端实时体验收到消息立即本地标记已读异步调用后端更新水位WebSocket 推送格式READ|maxSeq|readerId一次推送同步所有历史状态