直接用redistemplate.convertandsend()发消息客户端收不到,根本原因是redis pub/sub为广播机制,所有节点均收到消息,但仅持有目标websocketsession的节点应处理,其余节点须依据消息体中的userid等路由标识主动丢弃,避免重复推送或空转报错。

直接用 redisTemplate.convertAndSend() 向频道发消息,客户端收不到,不是Redis没发出去,而是所有节点都收到了、但只有持有目标 WebSocketSession 的那个节点该处理——其余节点必须主动丢弃,否则会重复推送或空转报错。
为什么监听同一频道会导致消息乱投
常见错误现象是:用户A连在 srv-a,用户B连在 srv-b;A发消息给B,srv-a调用 redisTemplate.convertAndSend("ws:msg", msg),srv-b确实收到了,但srv-a也收到了——它本地没有B的session,却仍尝试推送,抛出 IllegalStateException: The session has been closed 或静默失败。
- Redis Pub/Sub 本质是广播,不带路由能力,无法指定“只让 srv-b 收”
- 每个节点必须自己判断:“这条消息是不是我的活?”——依据是消息体里有没有本机缓存的
userId或deviceId - 频道命名不能泛化,比如别用
"ws:all",应按业务维度拆分:"ws:user"、"ws:order:1001",避免不同逻辑互相干扰
本地缓存 + Redis 路由映射怎么配才不冲突
关键不是“把 session 存 Redis”,而是分两层管理:本地内存存实时连接(快),Redis 存归属关系(稳)。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 连接建立时,把
userId → sessionId → WebSocketSession写进本地ConcurrentHashMap;同时往 Redis 写一条路由记录:SET ws:user:1001 "srv-a7f2" - 推送前先查 Redis:
redisTemplate.opsForValue().get("ws:user:1001"),如果返回值等于本机serverId,就从本地缓存取 session 直推;否则走 Pub/Sub 广播 - 不要把整个
WebSocketSession序列化存 Redis——它不可序列化,且网络传输开销大;只存轻量标识即可
消息体必须带哪些字段才能精准过滤
光有频道名不够,消息体本身得携带可校验的路由信息,否则节点无法决定是否处理。
- 最小必要字段:
{"userId":"1001","content":"hello","timestamp":1725418200};userId用于查本地缓存,timestamp用于幂等或过期判断 - 避免用
sessionId做主键:它每次重连都变,而userId稳定,更适合做路由锚点 - 如果业务需设备级隔离(如多端登录互踢),可加
"deviceId":"android-abc123",并用ws:device:android-abc123频道单独订阅
Pub/Sub 不可靠,怎么防丢又不卡主流程
Redis Pub/Sub 是内存型、无持久化、无 ACK 的机制,网络抖动或节点重启就会丢消息。不能把它当 MQ 用。
- 业务层要接受“可能丢一次”,补发逻辑由上游触发(例如前端超时重发,或服务端落库后异步重推)
- 不要在
MessageListener.onMessage()里做耗时操作(如查 DB、调远程接口),否则阻塞整个监听线程,新消息积压 - 真正需要可靠性时,改用 Redis Stream:
XADD ws:user:1001 * userId 1001 content "hello",配合消费者组 + pending list 实现至少一次投递
最容易被忽略的是:节点启动时没注册 serverId 到配置中心,或者连接断开后没及时清理本地缓存和 Redis 中的路由记录,导致消息持续发往已下线节点——这种 stale routing 比消息丢失更难排查。










