当前位置: 首页 > news >正文

Spring Boot + WebSocket 群聊已读未读:从 Demo 到生产级架构设计与落地

Spring Boot + WebSocket 群聊已读未读:从 Demo 到生产级架构设计与落地

一、前言:已读未读不是功能点,而是一套高并发状态系统

“群聊已读未读”看起来只是聊天界面底部的一行字:

  • 18 人已读
  • 237 人未读
  • 我是否已经看过最后一条消息

但一旦进入真实生产环境,这个问题很快就会从“前端展示逻辑”升级为“高并发状态同步系统”:

  • 一个 3000 人企业群,1 秒内可能有数百次已读推进
  • 用户同时在线于 Web、iOS、Android、桌面端,多端状态必须一致
  • WebSocket 连接分布在多个 Pod 上,推送不能依赖单机内存
  • 已读状态需要低延迟可见,但又不能把数据库写爆
  • 扩缩容、重连、消息乱序、重复投递、热点群,都会把简单方案打穿

所以,这个问题本质上不是“如何记录谁读了哪条消息”,而是:

如何在分布式系统中,以可扩展、可观测、可恢复的方式维护“用户阅读进度”这一核心状态。

本文会把这件事从原理、架构、工程、代码、案例五个层面完整讲透,并给出一套可落地的 Spring Boot + WebSocket + Redis + Kafka 生产方案。


二、业务语义先讲清:到底什么叫“已读”

在群聊系统里,“已读”有三种常见业务语义,它们不能混为一谈。

2.1 消息级已读

定义:用户是否读过某一条具体消息。

适合场景:

  • 小群
  • 需要查看“哪些人读了这条消息”
  • 合规审计、任务确认、流程审批类 IM

问题:

  • 如果每条消息都存已读用户列表,数据量会随“消息数 x 群成员数”爆炸增长

2.2 会话级已读

定义:用户在某个群聊里,已经读到哪一条消息。

典型表示:

(groupId, userId) -> lastReadMsgSeq

这是绝大多数生产 IM 的核心模型,因为:

  • 写入复杂度低
  • 查询未读数高效
  • 存储规模随“用户数 x 群数”增长,而不是随“消息数 x 用户数”增长

2.3 视图级已读

定义:不是“消息被拉到了客户端”就算已读,而是“消息进入了用户可视区域或用户进入了会话页面”才算已读。

这会影响:

  • 客户端上报时机
  • 回执语义是否可信
  • 多端状态推进的规则

生产里建议明确采用:

“客户端已可见 + 服务端幂等推进游标” 的定义。


三、先避坑:为什么很多方案一开始就设计错了

3.1 错误方案一:每条消息存一个已读用户集合

数据模型:

message_read_users:{msgId} -> Set<userId>

问题非常明显:

  • 1000 人群,1 天 10 万条消息,理论上可能产生 1 亿条关系
  • Redis Set 和数据库关联表都会迅速膨胀
  • 查询“某人未读多少条”反而更慢

这个模型只适合:

  • 成员规模小
  • 审批确认场景
  • 明确需要消息级已读明细

3.2 错误方案二:直接用 latestMsgId - lastReadMsgId

这个思路在 Demo 里常见,但生产里并不总是成立,因为会遇到:

  • 消息撤回
  • 审核删除
  • 分库分表后 ID 不连续
  • 不同分区消息序号并非全局自增

所以生产里不要把“数据库主键 ID”直接当未读计数依据。

正确做法是:

  • 为群内消息维护独立的单调递增 messageSeq
  • 已读推进也基于 messageSeq
  • 计数优先用 seq 差值,精确校准用补偿逻辑

3.3 错误方案三:已读上报直接同步写数据库

如果每次滚动列表都同步更新 MySQL/TiDB:

  • 数据库写放大严重
  • 峰值流量下 RT 抖动明显
  • 热门群会形成热点行更新

生产思路应该是:

  • 热路径先进 Redis
  • 异步写 Kafka
  • 消费端批量落库
  • 通过幂等更新保证最终一致

四、核心建模:群聊已读未读的正确抽象

4.1 关键对象

我们先把领域模型抽象清楚。

  1. Conversation 表示群聊会话。

  2. Message 表示群里的消息实体,除了数据库主键外,还应有群内递增序号 seq

  3. ReadCursor 表示某用户在某群中已经读到的最大消息序号。

4.2 推荐核心关系

Conversation: 1 ---- n Message Conversation: 1 ---- n ReadCursor ReadCursor: (conversationId, userId) -> lastReadSeq

4.3 为什么“游标法”是主流生产方案

游标法的核心不是记录“谁读了哪条”,而是记录“每个人读到了哪里”。

例如:

群 G 当前最新消息 seq = 1050 用户 U 的 lastReadSeq = 1043 则 U 的未读区间 = (1043, 1050]

它的优势非常稳定:

  • 写入是 O(1)
  • 更新天然幂等,可用 max(old, new) 合并
  • 用户维度查询非常快
  • 便于多端同步
  • 数据量可控

这也是生产架构能成立的根基。


五、生产级目标:这套系统到底要扛什么

一个可以上线的大规模群聊已读系统,至少要满足下面几类目标。

5.1 性能目标

  • 已读推进 RT:P99 < 100ms
  • 已读通知可见延迟:P99 < 300ms
  • 单群瞬时已读推进:每秒数百到数千次
  • 单节点长连接:1 万到 5 万,取决于协议栈和机器规格

5.2 一致性目标

  • 同一用户多端已读游标不回退
  • 已读事件允许重复,但最终状态不能错
  • 短时间允许最终一致,不要求分布式强一致事务

5.3 工程目标

  • 支持水平扩容
  • 支持热点群隔离
  • 支持故障恢复与数据回补
  • 支持链路追踪、指标告警、死信补偿

六、整体架构:从单机聊天室升级为分布式状态系统

6.1 推荐架构分层

客户端层 Web / iOS / Android / PC | 接入层 API Gateway / Ingress 鉴权、限流、WebSocket Upgrade、灰度路由 | 连接层 Push Gateway 负责 WebSocket/STOMP 连接管理、订阅关系、实时下行 | 业务层 Chat Service Read Service Conversation Service | 事件层 Kafka 消息发送事件、已读推进事件、成员变更事件 | 存储层 Redis Cluster TiDB / MySQL 分库分表 对象存储 / 冷归档

6.2 各服务职责

Push Gateway

  • 维护长连接
  • 处理 STOMP 订阅和下行推送
  • 不承担核心业务状态持久化

Chat Service

  • 消息发送
  • 消息持久化
  • 分配群内消息序号 seq

Read Service

  • 接收已读上报
  • 幂等推进 lastReadSeq
  • 产出已读事件
  • 维护 Redis 热状态

Conversation Service

  • 查询会话列表
  • 计算未读数
  • 汇总已读快照

6.3 为什么把“连接层”和“业务层”拆开

很多项目一开始把 WebSocket、消息发送、已读处理全部放在同一个 Spring Boot 服务里,初期没问题,规模上来后会出现三个硬伤:

  • 长连接占用线程、内存和 FD,影响普通业务接口稳定性
  • WebSocket 节点扩缩容频繁,和业务服务发布节奏不一致
  • 推送流量和业务读写流量耦合,难以独立扩容

所以生产里建议至少逻辑拆分成:

  • 连接网关
  • 消息业务
  • 已读业务

七、已读链路设计:从客户端上报到多端同步的完整流程

7.1 基础流程

1. 用户打开群聊或滚动消息列表 2. 客户端计算当前可见最大 messageSeq 3. 通过 STOMP 发送 read receipt 4. Push Gateway 鉴权并转发给 Read Service 5. Read Service 用 Redis Lua 脚本执行“只增不减”更新 6. 更新成功后发送 Kafka ReadCursorAdvancedEvent 7. Push Gateway 订阅事件,向同用户其他端和群内在线成员推送增量通知 8. 消费者批量落库,形成可恢复状态

7.2 为什么一定要“只增不减”

真实场

http://www.jsqmd.com/news/893384/

相关文章:

  • 从能量搬运工到效率管家:深入剖析Boost电路的设计要点与效率优化
  • 广州海珠区搬家公司推荐 搬家杂物快速整理全攻略 - 从来都是英雄出少年
  • VLOOKUP核心原理与防错实战:从查找匹配到跨表关联
  • UPS、光伏逆变器、电焊机:FGZ75XS65C的650V IGBT应用版图
  • Go语言安全编程入门指南
  • 2026年 钕铁硼磁铁厂家推荐榜:烧结/粘结/N52/微型磁铁及精密组件供应商深度解析与选购指南 - 品牌企业推荐师(官方)
  • Linux内核开发避坑指南:手把手教你理解container_of宏的魔法
  • 手把手教你用C语言和libusb库实现Android AOA协议通信(附完整项目代码)
  • Go语言加密技术深度解析
  • HFSS新手避坑指南:手把手教你仿真2.45GHz侧馈微带天线(附FR4板材参数)
  • 2026实测:视频号视频怎么保存到相册?苹果安卓方法全攻略
  • 2026年钕铁硼/钐钴磁铁/强磁铁厂家推荐榜:异形、耐高温、沉孔磁铁及橡胶、铁氧体、铝镍钴磁铁优质品牌精选 - 品牌企业推荐师(官方)
  • Redis看门狗机制详解(原理+源码+踩坑+面试全覆盖)
  • AI应用开发学习路径/50W年薪构成
  • 面试鸭:你的面试通关加速器,1万+高频题库免费刷
  • Windows智能家居客户端HASS.Agent完整配置指南:实现PC与Home Assistant无缝集成
  • 极域电子教室UDP广播风暴治理三步法
  • Go语言安全最佳实践与漏洞案例分析
  • 2026年5月河北喷嘴流量计生产厂家哪个好?这家企业值得重点关注 - 2026年企业资讯
  • MongoDB 复制(副本集)
  • Django 从 0 到 1 打造完整电商平台:HTTPS 配置与域名绑定
  • Hermes Agent 安装教程:多平台接入与网关配置详解
  • 大语言模型与混合集成架构在司法裁决预测中的应用与实践
  • 基于深度嵌入聚类与序列自编码的无监督日志异常检测方案LogDEC
  • 海珠区搬家公司电话 高端搬家与普通搬家区别详解 - 从来都是英雄出少年
  • 2026最新视频号视频保存到相册方法多种实用技巧分享
  • Go语言Web安全防护实战
  • 中文文献管理难题如何破解?Jasminum为Zotero带来智能化解决方案
  • Power BI中用DAX构建可配置的周末与周边界识别体系
  • 嵌入式NAND闪存文件系统选型:JFFS2、YAFFS2与UBIFS深度对比