WebSocket消息重复是协议和网络层固有特性所致,需通过Redis复合指纹校验、数据库唯一索引及状态机三重防护实现幂等。

WebSocket 本身不保证消息不重复,必须在应用层做幂等控制;靠客户端单点缓存或服务端简单去重,线上扛不住千万级并发。
为什么 WebSocket 消息会重复?
不是你代码写错了,是协议和网络层天然存在多处重复触发点:
- WebSocket 连接断开重连时,客户端可能重发未确认的业务消息(比如礼物点击)
- 服务端负载均衡器在超时后自动重试转发,同一消息被投递两次
- 后端服务实例重启或滚动发布,导致部分消息被 Kafka/RabbitMQ 重复投递
- 客户端网络抖动触发前端 SDK 自动重发,而
onmessage回调无感知
关键点:msgId 和 traceId 都不能单独作为去重依据——知识库中明确提到有 0.7% 的消息 msgId 相同但 traceId 不同,反之亦然。
用 Redis + 复合指纹实现服务端幂等校验
客户端传来的每条消息,必须携带可组合的业务唯一标识。推荐用 ${msg.common.msgId}_${msg.traceId}_${msg.user.idStr} 作为指纹 key,而非只拼 msgId。
- Redis 使用
SET key "1" EX 30 NX原子写入,30 秒过期(需 ≥ 最大重试窗口) - 若写入失败(返回
null),说明该指纹已存在,直接return或丢弃 - 不要在消费成功后再删 key——万一处理中途崩溃,key 会残留导致漏处理;应靠 TTL 自动清理
- 高并发下避免用
GET + SET两步操作,那是竞态漏洞源头
示例(Java Spring Boot):
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
String fingerprint = String.format("%s_%s_%s",
msg.getCommon().getMsgId(),
msg.getTraceId(),
msg.getUser().getIdStr());
Boolean isDuplicate = redisTemplate.opsForValue()
.setIfAbsent("ws:idempotent:" + fingerprint, "1", Duration.ofSeconds(30));
客户端也要做轻量级防抖,但不能依赖它
前端加一层 localStorage 缓存能过滤掉用户手抖、按钮未禁用等低级重复,但绝不能把它当主力防线:
- 缓存容量有限(
LRUMap(1000)是合理上限),超出即淘汰,无法覆盖长周期重连场景 - 用户清缓存、换设备、多标签页并行时,缓存完全失效
- 必须配合服务端校验,否则「缓存没命中 → 放行 → 服务端又没校验 → 重复入库」就成定式了
别把 checkDuplicate() 当兜底逻辑,它只是第一道过滤网。
状态机 + 唯一索引才是最终保险
Redis 校验通过后,业务逻辑仍要双重防护:
- 数据库写入前,对核心表(如
gift_log)加联合唯一索引:UNIQUE (msg_id, trace_id, user_id) - 更新类操作走状态机判断,例如只有
status = 'pending'才允许转为'sent',用UPDATE ... WHERE status = ?影响行为 0 行来识别重复 - 别信“一次写入就完事”——某支付系统事故就是因没加唯一索引,同一订单回调被处理两次,直接扣双份款
真正难的不是加几行代码,而是想清楚:哪一层出问题概率最高?哪一层失效后果最重?然后让 Redis、DB、状态校验各守一段,别把所有鸡蛋放在一个篮子里。

















