Kafka 不能和 WebSocket Session 直连推送,否则单机撑不过 5000 连接就会丢消息、OOM 或触发频繁 rebalance;根本原因是 Kafka 消费者线程的同步阻塞模型与 session.sendText() 的 I/O 不确定性冲突,且 Session 非线程安全、断连不自动清理,必须通过异步队列+独立推送线程池+ConcurrentHashMap 管理会话来解耦。

直接上结论:Kafka 不能和 WebSocket Session 直连推送,否则单机撑不过 5000 连接就会丢消息、OOM 或触发频繁 rebalance——这不是参数调得不够好,是线程模型和资源语义根本冲突。
为什么 Kafka 消费者线程不能调用 session.sendText()
Kafka 消费者线程(KafkaConsumer.poll() 循环)本质是同步阻塞模型,而 session.sendText() 可能因网络缓冲区满、SSL 加密耗时、客户端断连未清理等原因卡住。一旦某个 session 卡住,整个消费者线程就停摆,直接导致 max.poll.interval.ms 超时,触发 rebalance;同时 Session 非线程安全,多线程并发调用会抛 IllegalStateException。
- 断连的
Session不会自动从静态集合中移除,后续持续调用sendText()会堆积异常、吃光线程池 -
CopyOnWriteArraySet存万级Session会导致 GC 压力暴增,改用ConcurrentHashMap<String, Session>,key 为session.getId() - 必须在发送前校验
session.isOpen(),异常捕获后立即从 map 中remove()
如何解耦消费与推送:推荐异步队列 + 单独推送线程池
核心原则是:@KafkaListener 方法里只做解析、去重、校验,然后立刻入队;所有 Session 操作必须收口到一个独立的推送服务中。
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- 用
ConcurrentLinkedQueue或 Spring 的ApplicationEventPublisher中转消息,避免阻塞消费者线程 - 另起一个
@Service类,用@EventListener或固定线程池轮询队列,取出消息后查ConcurrentHashMap获取Session - 生产者发 Kafka 消息时,必须把目标
sessionId或userId设为 record 的key,否则无法路由到对应会话 - 不依赖 Kafka 分区或 group.id 实现“按人分发”,那是业务层该干的事
分布式环境下怎么保证消息只推一次且不丢
多个 WebSocket 实例同时消费同一个 Topic,必然重复处理。靠“每个实例都查 Redis 判断在线”只是缓解,不是根治——真正可靠的做法是让消息精准路由到持有该 Session 的唯一实例。
- 用户连接时,将
clientId → instanceId写入 Redis(带过期时间),比如SET ws:route:u123 "ws-gateway-2" - 消息进入 Kafka 后,由网关层或单独的 Router Service 根据
clientId查出目标实例,再投递到对应子 Topic(如topic_push_ws-gateway-2) - 每个实例只订阅自己的子 Topic,彻底避免重复消费
- Redis 路由信息需配合心跳保活,超时未续期则视为离线,走未读补发流程
最易被忽略的一点:WebSocket 只管通道,不保消息。哪怕推送成功,客户端也可能因网络抖动没收到。真正的可靠性必须靠 Kafka 消息可重放 + TiDB/Redis 落库 + 客户端 ACK 回写闭环,而不是在 sendText() 上死磕重试逻辑。


















