subscribe 不能单独用于服务发现,因其无状态、不通知断连、无心跳机制且无法区分上下线事件;可靠方案是用 set+expire 存心跳,keyevent 监听过期,publish 广播变更。

SUBSCRIBE 和 PUBLISH 本身不支持服务发现,但可以组合 SET + EXPIRE + KEYEVENT 实现节点存活感知;真正做服务发现时,必须把“发布订阅”仅当作事件广播通道,而不能依赖它来维持节点状态。
为什么不能只靠 SUBSCRIBE 做服务发现
Redis 的发布订阅是纯内存、无状态的:一旦客户端断开(网络抖动、进程崩溃、超时未读),Redis 不会通知其他订阅者,也不会保留该节点的任何上下文。这意味着:
-
SUBSCRIBE连接断开后,不会触发任何回调或事件,你无法知道“谁掉线了” - 没有内置的 TTL 或心跳绑定机制,
SUBSCRIBE本身不记录节点元数据(IP、端口、权重、健康状态) - 多个订阅者收到相同消息,但无法区分“这是新上线”还是“这是重连后的心跳”,缺乏幂等性控制
用 SET + EXPIRE 存节点心跳,才是可靠基础
每个服务实例启动后,应周期性执行:
SET service:node-001 "192.168.1.101:8080" EX 15
其中 EX 15 表示 15 秒过期,配合每 5 秒发一次心跳,可容忍 2 次丢包仍不误判下线。
关键点:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 键名要带唯一标识(如 hostname + pid),避免多实例覆盖
- 值建议存 JSON 或结构化字符串,至少含
ip、port、timestamp,方便后续扩展 - 不要用
SETEX,它不是原子命令;优先用SET key value EX seconds(Redis 2.6.12+)保证原子性 - 若使用 Redis 集群,确保所有节点心跳 key 落在同一个哈希槽(比如加
{service}前缀)
用 __keyevent@0__:expired 监听下线事件,再广播
单独起一个长期运行的监听进程,订阅 Redis 的键事件:
PSUBSCRIBE __keyevent@0__:expired
当 service:node-001 过期时,会收到类似消息:
["pmessage", "__keyevent@0__:expired", "__keyevent@0__:expired", "service:node-001"]
此时你可以:
- 查
service:node-001是否真不存在(防误触发),再调用PUBLISH service:discovery "offline:node-001" - 主动拉取当前全部
service:node-*列表(用SCAN),推给所有监听service:discovery的客户端 - 注意:键事件默认关闭,需在 redis.conf 中开启
notify-keyspace-events Ex(E=event,x=expired)
客户端如何安全地订阅并更新本地服务列表
订阅者不应只监听 service:discovery,而要结合本地缓存和兜底策略:
- 首次启动时,先
SCAN 0 MATCH service:node-* COUNT 100拉全量,再SUBSCRIBE service:discovery - 收到
"online:node-002"消息,解析后加入内存 map;收到"offline:node-002"就从 map 删除 - 定期(如每 30 秒)重新
SCAN全量比对,防止因网络丢消息导致状态漂移 - 不要在
message回调里做耗时操作(如 HTTP 请求),应投递到本地队列异步处理
真实场景中,最易被忽略的是键事件的可靠性边界:它依赖 Redis 单点或哨兵主节点的稳定性;如果整个 Redis 故障,EXPIRE 不触发,<strong>keyevent</strong> 断流,此时服务发现就完全失效——所以生产环境必须搭配哨兵或 Cluster,并让客户端有降级逻辑(例如 fallback 到配置中心或本地静态列表)。










