离线消息需确保用户上线后完整、有序、不重复接收,须通过心跳+超时机制区分真/假离线,MySQL持久化存储消息(含状态、序号等字段),Redis仅作轻量索引,配合ACK确认、幂等去重与多端状态同步。
离线消息不是“推完就算”,而是要确保用户上线后能完整、有序、不重复地收到所有错过的消息。单纯靠客户端重连后拉取,或服务端盲目广播,都会在多端、弱网、快速切换等真实场景下崩盘。
WebSocket连接断开时,服务端怎么判断用户是“真离线”还是“假掉线”
直接删掉 clients 里的 Session 是最常见错误。网络抖动、页面刷新、APP切后台都可能触发 onclose,但用户几秒内就回来了——这时候删 Session,等于主动丢弃后续所有消息路由能力。
- 必须引入心跳 + 超时兜底机制:客户端每 30 秒发
ping,服务端收到后标记lastHeartbeatTime;若 90 秒未更新,才视为“疑似离线” -
onclose触发时,只做软下线:保留 Session 引用、不清空userId → ws映射,仅标记status = "offline_pending" - 真正清理 Session 的时机,应是超时检查线程确认无心跳且无新连接复用时,再从内存映射中移除
- Spring Boot 中建议用
@Scheduled(fixedDelay = 10000)做异步扫描,避免阻塞 I/O 线程
离线消息存哪里?MySQL 还是 Redis?选错直接影响吞吐和一致性
Redis 适合做临时缓存,但不适合当离线消息的唯一存储。它没有事务、不支持复杂查询、数据易丢失,一旦 Redis 故障或主从同步延迟,消息就没了。
- 生产环境必须落库:用 MySQL 存
message表,字段至少包含id、to_user_id、from_user_id、content、status(0=待投递,1=已送达)、created_at、seq_no(用于排序) - Redis 只做轻量索引:用
SET存offline:u123,记录该用户有离线消息;用ZSET存offline_queue:u123,按seq_no排序,加速上线拉取 - 写消息时,先写 MySQL(带事务),再更新 Redis 索引;读消息时,先查 Redis 判断有无离线,再按范围查 MySQL 分页拉取
- 别用 Redis 的
PUBLISH/SUBSCRIBE做跨节点通知——它不保证可达,集群里容易漏事件
用户多端登录时,“全量同步”到底同步什么?不是把所有历史都扔过去
“全量同步”这个词容易误导人。真实业务里,你不会也不该把用户三年前的聊天记录一股脑推给刚登录的 iPad —— 那是资源浪费,更是性能灾难。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- 真正的全量同步,是指“当前会话上下文的完整状态同步”:包括未读数、最新 N 条消息(如 50 条)、会话列表顺序、未读会话 ID 列表
- 每个设备首次连接时,只拉取
last_active_time之后产生的消息,配合seq_no做幂等去重 - 多端同时在线时,某端已读一条消息,需通过 Redis Pub/Sub 或 WebSocket Server 内部广播,通知其他在线终端更新本地
read_seq - 关键点:不同设备间的状态同步,必须走服务端中转,不能靠客户端互相通信 —— 否则设备 A 没收到通知,就会一直显示“1条未读”
为什么 ACK 机制不能只靠前端回调?后端必须自己验
前端调 socket.send() 成功,不代表消息真到了用户眼里。页面可能卡死、JS 执行被挂起、甚至用户根本没看到弹窗提示。
- ACK 必须由客户端显式上报,例如收到消息后发一条
{"type":"ack","msg_id":"abc123"},服务端收到才更新 MySQL 的status - 服务端要配超时重发:对 5 分钟未 ACK 的消息,重新投递(但最多重试 3 次,避免雪崩)
- 重试时不能简单再塞进 Redis 队列——得查 MySQL 确认是否已送达,否则会重复推送
- 前端 ACK 上报失败(比如网络又断了),服务端需依赖下次上线时的“补推+校验”流程兜底,而不是无限等待
离线消息最难的从来不是“存起来”,而是“什么时候取、按什么顺序取、取完怎么标记、取错怎么回滚”。每个环节都要有可验证的状态机,而不是靠 try-catch 和日志猜。

















