news 2026/5/27 0:24:11

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 为什么一定要“只增不减”

真实场

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/27 0:18:03

SQL示例(使用差分数组 + 窗口函数)统计并发数量问题(处理边界:当开始时间和结束时间相同时,应该先+1再-1,才能正确统计峰值)

本文详细讲解了如何计算视频的最大并发播放量并获取TOP3视频。核心思路是将观看记录拆分为开始(1)和结束(-1)事件&#xff0c;通过窗口函数累加计算各时刻的并发人数&#xff0c;再取最大值。文章提供了两种SQL实现方案&#xff0c;重点优化了边界情况处理&#xff08;如时间相…

作者头像 李华
网站建设 2026/5/27 0:16:03

基于进化信息与XGBoost的淀粉样蛋白预测:特征工程与模型构建全解析

1. 项目概述与核心挑战在生物信息学领域&#xff0c;预测蛋白质是否具有形成淀粉样纤维的倾向&#xff0c;即淀粉样蛋白&#xff08;Amyloid Proteins, AMYs&#xff09;预测&#xff0c;是一个极具挑战性且意义重大的课题。这类蛋白质的错误折叠与异常沉积与阿尔茨海默症、帕金…

作者头像 李华
网站建设 2026/5/27 0:09:28

Kubernetes持续集成与持续交付最佳实践:构建自动化部署流水线

Kubernetes持续集成与持续交付最佳实践&#xff1a;构建自动化部署流水线 一、CI/CD概述 **CI/CD&#xff08;持续集成/持续交付&#xff09;**是一种自动化软件交付的方法论&#xff0c;在Kubernetes环境中集成CI/CD可以实现应用的自动化构建、测试和部署。 1.1 CI/CD流程 …

作者头像 李华
网站建设 2026/5/27 0:07:54

为OpenClaw智能体配置Taotoken作为自定义模型供应商

&#x1f680; 告别海外账号与网络限制&#xff01;稳定直连全球优质大模型&#xff0c;限时半价接入中。 &#x1f449; 点击领取海量免费额度 为OpenClaw智能体配置Taotoken作为自定义模型供应商 本文面向使用OpenClaw框架开发AI智能体的开发者&#xff0c;介绍如何将Taotok…

作者头像 李华