news 2026/6/19 9:13:49

好友聊天已读状态总结

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
好友聊天已读状态总结

在即时通讯系统中,用户需要知道发送的消息是否被对方阅读。传统方案是为每条消息单独存储 is_read 字段,但这种方式在高并发场景下会导致数据库压力过大——每收到一条消息就要更新一条记录,消息量增长后性能急剧下降。
为解决这个问题,我借鉴了一些大佬的想法,基于IM系统的设计,采用会话维度水位记录而不是逐条标记;

核心思想: - 每条消息在会话内分配单调递增序号 seq - 每个用户在每个会话只存一条"阅读水位"记录 - 判断规则:消息.seq <= 用户.last_read_seq → 已读

方案对比:

数据库设计:

  1. 消息表新增序号列
ALTER TABLE chat_msg ADD COLUMN seq BIGINT NOT NULL DEFAULT 0 COMMENT '会话内单调递增序号';
  1. 会话序号分配表
CREATE TABLE conversation_seq ( conversation_key VARCHAR(50) PRIMARY KEY COMMENT '会话标识:小ID_大ID', seq BIGINT NOT NULL DEFAULT 0 COMMENT '下一条消息的序号' );
  1. 会话阅读水位表
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); // 查询消息列表 List<ChatMsg> 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 Result<Void> 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,一次推送同步所有历史状态
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/19 9:01:07

YOLOv8桥梁八类病害检测:工程化落地实战指南

1. 为什么桥梁病害检测不能只靠“人眼望远镜”&#xff1f;我第一次站在沪昆高速某特大桥桥墩下&#xff0c;仰头看那道3米长的斜向裂缝时&#xff0c;手里的检测记录本是空的。不是没看见——是根本不敢写。因为按《公路桥梁技术状况评定标准》&#xff08;JTG/T H21-2011&…

作者头像 李华
网站建设 2026/6/19 8:55:18

哥斯拉Webshell管理工具:绕过WAF与静态查杀的实战指南

1. 项目概述&#xff1a;为什么我们需要哥斯拉&#xff1f; 在Web安全测试与应急响应的实战中&#xff0c;Webshell的管理工具一直是攻防两端博弈的焦点。从业内经典的“菜刀”&#xff0c;到后来加密流量、功能强大的“冰蝎”和插件生态丰富的“蚁剑”&#xff0c;红队和渗透测…

作者头像 李华
网站建设 2026/6/19 8:51:58

MLOps 5代高效范围界定:从模糊需求到契约式Scoping

1. 项目概述&#xff1a;为什么“高效范围界定”是MLOps落地的第一道生死线 你有没有遇到过这样的场景&#xff1a;团队花了三个月训练出一个AUC高达0.92的信用评分模型&#xff0c;上线后发现——它根本没法接入现有风控系统API&#xff1b;或者数据科学家反复优化F1-score&am…

作者头像 李华
网站建设 2026/6/19 8:48:45

2026年AI编码平替实战指南:本地化、离线化与VS Code原生能力深度整合

1. 为什么2026年必须重新审视“Copilot平替”这个命题2026年4月&#xff0c;我清理VS Code插件列表时删掉了第三个Copilot类工具——不是因为不好用&#xff0c;而是因为它们全在同一天弹出“免费额度已用尽”的提示框。那一刻我意识到&#xff1a;所谓“永久免费且能力更强”的…

作者头像 李华
网站建设 2026/6/19 8:48:35

Gemma 4端云协同架构解析:轻量LLM如何赋能AI-Agent落地

1. 这不是又一个“更强更大”的模型&#xff0c;而是一套端云协同的精密齿轮组最近两周&#xff0c;我几乎没碰过其他模型&#xff0c;就泡在 Gemma 4 的文档、社区讨论和本地实测里。不是因为它参数多吓人——31B 在今天确实不算顶流&#xff1b;也不是因为它跑分多亮眼——在…

作者头像 李华
网站建设 2026/6/19 8:45:52

LangGraph重试策略架构设计:构建高可用AI工作流的容错机制

LangGraph重试策略架构设计&#xff1a;构建高可用AI工作流的容错机制 【免费下载链接】langgraph Build resilient agents. 项目地址: https://gitcode.com/GitHub_Trending/la/langgraph 在分布式AI系统中&#xff0c;网络波动、API限制和资源竞争等不可预测因素常常导…

作者头像 李华