关注我的公众号:【编程朝花夕拾】,可获取首发内容。
01 引言
这两天整理了一个simonking-ws/stream-nexus项目,原本定位就是一个轻量级的应用,不想使用任何第三方的中间件。
介绍项目的相关文档发不出后,有老铁问到能否集群部署。显然目前是不能的满足的。本节就聊聊实现的思路,后面时间允许的话也安排上。
02 实现思路
刚看这位老铁的提问,我本能的反应,这个简单呀,直接把客户端的连接从内存搬到中间件(Redis)里面去就好了。
2.1 简单测试
鉴于开发的习惯,总要先写个Demo测试一下。测试之后才发现根本行不通。

分别使用内存的对象和Redis中的对象发送,结果只收到内存对象的推送:

我们看看Redis中保存的是什么?

Redis对象中只保留了客户端中保留了超时时间,显然是很多参数丢失了。
2.2 原因
应该会有和我一样想法的老铁吧!哈哈哈......
归根结底是因为连接是"活的"对象,Redis 是"死的"键值存储,两者根本不是一回事。连接本质上是"内核态对象",序列化没用。
一条 SSE/WebSocket 连接至少由这些东西组成:
| 部分 | 归属 | 可序列化吗 |
|---|---|---|
| TCP Socket fd / 端口号 | OS 内核 | ❌ |
| TCP 收发缓冲区、滑动窗口状态 | OS 内核 + 网卡驱动 | ❌ |
| TLS Session、加密密钥、序列号 | 协议栈 | ❌ |
| HTTP/1.1 解析状态、chunked 编码位置 | Servlet 容器 | ❌ |
SseEmitter / WebSocketSession 句柄 |
应用容器 | ❌ |
一个连接从握手成功那一刻起,就和"建立它的那台机器的操作系统、JVM、Servlet 容器、网卡"绑死了。
当把 clientId → 8088机器的连接 这条映射关系序列化到 Redis 里,Redis 存的只是一条字符串,它指向的那根 TCP 还在原机器上。一旦那台机器宕机/重启/扩缩容,Redis 里这条映射就变成"幽灵指针"。
也就是说Redis存的只是"路由表",但真正的连接根本拿不走。
2.3 高可用方案
既然连接是基于内核、容器的,那就只能保存在当前的机器的进程中。高可用方案无非就是通过到各台机器触发自己的客户端推送消息即可。
可以通过Redis或者MQ等其他中间件,作为消息总线。如Redis
text
业务系统 ──HTTP POST──▶ /push
│
▼
推送入口网关 (任一实例)
│
▼
Redis Pub/Sub (channel: biz:order:paid)
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
sse/ws-1 sse/ws-2 sse/ws-3
本地连接表查找 本地连接表查找 本地连接表查找
│ │ │
▼ ▼ ▼
客户端A 客户端B 客户端C
关键设计:
- 推送入口统一化,业务系统只调一次推送接口
- Redis Pub/Sub 的 channel 划分:
biz:module:{bizModule}------ 按业务模块广播biz:client:{clientId}------ 定向推送- 推送时同时发到两类 channel,各实例根据本地订阅关系决定是否落地。
- 本地查找:每台实例订阅全量 channel,收到消息后只在自己的连接表里命中目标
03 单体与集群
程序代码的高可用深入人心,面试更是逃不掉的八股文。很多人都觉得集群就是比单体的项目牛逼,的确是的,集群就是牛逼。
但是,这个要看真实的业务场景。比如天猫、京东这类大厂,单体项目自然无法支撑,集群是必然选择,大厂里面有无数牛逼的大佬专门通过中间件维护者集群的高可用。
而作为一般公司,业务量没有那么大时,单体项目也可以支撑。但是项目没有复杂的中间件,维护成本很低。如果硬要上集群,当然也没问题,只不过增加了维护的成本。
所以,单体未必差、集群未必好!适合自己的项目就行。系统不是设计出来了,是不断迭代演化而来的。
04 小结
我一直在想Stream Nexus要不要去支持集群,集群必然需要引入中间件。好纠结呀!
Redis似乎已经是很多企业一定有的中间件,通过Redis的Pub/Sub 广播当做消息总线似乎也是一种不错的选择。
后续根据情况,可以支持(但不必须)集群,提供多种选择,完善项目。希望对正在折腾推送服务的老铁们有帮助。