结论:由于 Redis 的 SUBSCRIBE 具备纯内存、无状态、不保存元数据、无法感知连接中断等特征,因此它本身并不适合直接承担服务发现能力。要实现可靠的服务注册与服务发现,必须基于 SET + EXPIRE + KEYEVENT 机制,通过设置带有 TTL 的键来维护节点存活状态,并使用 PSUBSCRIBE __keyevent@0__:expired 监听过期事件,从而触发节点上下线通知与广播。

直接说结论:SUBSCRIBE 本身不能做服务发现
Redis 的 SUBSCRIBE 本质上是纯内存、无状态的消息广播通道,不会记录服务节点元数据,无法感知客户端断连,也不具备服务上线、下线的语义。如果把它直接当作服务注册中心来做服务发现,就等于把节点心跳和存活判断交给不确定因素——一旦发生网络抖动、进程崩溃,或订阅端消费延迟,就很难准确判断到底是哪台节点已经离线。
为什么 SET + EXPIRE + KEYEVENT 才是可靠基础
真正能支撑 Redis 服务发现机制稳定运行的,不是发布订阅本身,而是 Redis 的键生命周期管理能力。发布订阅只适合负责广播节点变更事件,真正的状态维护必须依赖可持久验证、可检测、带 TTL 的键操作:
SET service:node-{id} "{ip}:{port}" EX 15:每个服务节点每 5 秒续一次心跳,过期时间设置为 15 秒,可以容忍连续 2 次心跳丢失- 键名必须包含唯一标识(例如
hostname:pid),否则多个实例同时注册时可能互相覆盖,影响服务节点识别 - 值建议使用 JSON 格式,至少包含
ip、port、timestamp等字段,便于后续实现健康检查、节点筛选或权重路由 - 必须使用
SET key value EX seconds这样的原子命令,不建议使用SETEX(兼容性和实践层面都不如前者,且 Redis 早期版本中已逐步弱化) - 如果采用 Redis 集群部署,所有心跳 key 应尽量落在同一哈希槽中,可通过增加
{service}前缀实现,例如{service}:node-001
如何监听下线事件并广播变更
通过监听 __keyevent@0__:expired 键过期事件,才是触发“服务节点下线通知”的正确方式,也是 Redis 发布订阅结合 TTL 做服务发现的关键步骤:
- 单独启动一个长连接监听进程,执行
PSUBSCRIBE __keyevent@0__:expired - 当收到类似
["pmessage","__keyevent@0__:expired","__keyevent@0__:expired","service:node-001"]的消息后,先执行EXISTS service:node-001确认该键是否确实已过期,避免误触发 - 确认节点已失效后,再调用
PUBLISH service:discovery "offline:node-001"广播下线事件,通知其他服务消费者更新节点列表 - 而上线事件应由节点在启动时主动执行
PUBLISH service:discovery "online:node-001"进行广播,不要依赖订阅方自行轮询或扫描发现
容易被忽略的三个实操坑
很多团队在 Redis 服务发现方案落地和调试时容易踩坑,问题通常集中在配置细节和边界处理上:
- Redis 默认不会开启键事件通知,必须显式配置:
notify-keyspace-events Ex(其中E代表键事件,x代表过期事件)。无论通过redis.conf还是CONFIG SET设置,都要注意重启后的配置持久化问题 PSUBSCRIBE所使用的连接不能复用,因为订阅后该连接会被阻塞,无法继续执行其他 Redis 命令;因此必须专门用于监听,另外再创建连接处理PUBLISH、SET等操作- 如果没有“上线即广播”的补偿逻辑,就可能出现节点已经注册成功但监听方未及时感知的问题:正确顺序应该是节点启动后先执行
SET写入心跳,再主动PUBLISH online
服务发现的核心本质是服务状态同步,而不是单纯的消息通知。把 SUBSCRIBE 当成服务发现主干,相当于把系统基础建立在不可靠的广播机制上;真正稳定、可验证的做法,始终是带 TTL 的键状态维护 + 显式事件驱动 + 必要时配合主动探测兜底。
