websocket消息顺序与不丢失需靠client_seq+ack+server_seq原子共识实现:客户端用单调递增持久化计数器,服务端以落库后的严格递增server_seq为排序唯一权威,接收端按server_seq滑动窗口重排,重连时先同步历史再补发。

WebSocket 连接本身不保证应用层消息的顺序性和不丢失,必须靠自定义 SeqID 机制 + ACK + 状态管理来兜底。核心不是“加个序号”,而是让前后端对“哪条消息该在什么时候生效”达成原子级共识。
客户端 SeqID 必须单调递增且持久化
不能用 Date.now()、Math.random() 或 UUID——它们无法排序,也容易重复或倒流。正确做法是:
- 用整数计数器,每次发消息前
++seq,并立即存入localStorage或IndexedDB(重连后读取恢复) - 每个连接独占一个计数器,多标签页/Worker 场景下需用
SharedWorker或广播通道同步状态 - 消息体必须同时携带:
client_seq(用于去重和本地重发控制)、messageId(推荐crypto.randomUUID(),用于幂等识别)
服务端用 server_seq 做最终排序权威
客户端的 client_seq 只是临时标记;服务端在业务处理完成、数据落库后,才赋予该消息一个全局唯一、严格递增的 server_seq(如 MySQL 自增 ID 或 Snowflake ID)。所有排序、渲染、归档都以它为准。
- 服务端收到消息后立刻返回
{"type":"ack","client_seq":1001},不等待业务执行完成 - 落库成功后,推送带
server_seq的结果消息(如{"type":"update","data":...,"server_seq":5023}) - 所有下游通道(WebSocket 推送、离线拉取、轮询响应)统一按
server_seq归并排序,不混用其他字段
接收端用缓冲队列 + 滑动窗口做重排
不能一收到 onmessage 就更新 UI。要先缓存,再按 server_seq 连续提交:
- 维护
nextExpected = 1和pendingMap: Map<number msg></number> - 收到消息:若
msg.server_seq → 丢弃(已处理过);若远大于 <code>nextExpected + 64(滑动窗口上限)→ 拒收或告警 - 插入
pendingMap后,循环检查pendingMap.has(nextExpected),连续段批量取出、提交、递增nextExpected - 对卡住超过 5 秒的
server_seq,触发重发请求并标记超时
重连时先同步历史,再补发未确认
重连成功 ≠ 状态一致。错误做法是直接重发未 ACK 的消息,这会导致“旧结果晚于新结果”显示。
- 重连后第一帧发送
{"type":"fetch_offline","last_server_seq":5022} - 服务端按
server_seq > last_server_seq升序返回离线消息列表 - 客户端逐条写入缓冲区,等全部
confirmed后,再从本地待确认队列中按原始client_seq顺序重发未 ACK 消息 - 每条重发消息追加
_retry_1后缀,便于前端跳过重复渲染逻辑
不复杂但容易忽略:真正难的不是写排序代码,而是前后端共同约定“confirmed”的语义——它必须代表该 server_seq 及之前所有序号均已提交、无 gap、且已持久化。任何一环松动,UI 就会错乱。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











