直接用 SET + 过期时间不够可靠,因无法原子性兼顾首次注册与续期,易致并发覆盖、残留旧实例或崩溃未删键;需用 Lua 脚本封装 EXISTS 判断与 SET/EXPIRE 原子操作,并结合 SCAN+TTL 低开销健康检查及代理层安全读取。

为什么直接用 SET + 过期时间不够可靠
微服务心跳不是简单“存个键就完事”。常见做法是用 SET key value EX 30 NX,但实际部署中会遇到:多个实例并发写入导致覆盖、网络分区时旧实例残留、客户端崩溃后未主动删键。Redis 本身不提供原子性的“刷新过期时间 + 条件更新”能力,SET 的 NX 和 XX 无法组合使用,所以单靠一条命令没法兼顾首次注册和续期。
用 SET + Lua 脚本统一处理注册与续期
把“首次设置”和“存在则更新过期时间”封装成一个原子操作。Lua 脚本在 Redis 服务端执行,避免竞态:
if redis.call('EXISTS', KEYS[1]) == 0 then
return redis.call('SET', KEYS[1], ARGV[1], 'EX', ARGV[2])
else
return redis.call('EXPIRE', KEYS[1], ARGV[2])
endGo 中调用示例:
script := redis.NewScript(1, luaScript)
_, err := script.Do(ctx, rdb, []string{"service:auth:192.168.1.10:8080"}, "up", "30").Result()-
KEYS[1]是服务唯一标识(建议含 IP+端口+服务名) -
ARGV[2]是 TTL,必须每次传真实值(不能硬编码),便于动态调整 - 脚本返回值为
1表示新注册,0表示仅续期,可用于日志追踪
如何用 SCAN + TTL 做低开销的健康检查
别用 KEYS service:* ——集群模式下会阻塞,且数据量大时超时。改用 SCAN 分批遍历,再对每个 key 调用 TTL 判断是否过期:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
立即学习“go语言免费学习笔记(深入)”;
cursor := uint64(0)
for {
var keys []string
cursor, err = rdb.Scan(ctx, cursor, "service:*", 100).Result()
if err != nil { break }
if len(keys) == 0 { break }
<p>// 批量查 TTL(用 pipeline 减少 RTT)
pipe := rdb.Pipeline()
for <em>, key := range keys {
pipe.TTL(ctx, key)
}
cmders, </em> := pipe.Exec(ctx)
for i, cmder := range cmders {
ttl, _ := cmder.(<em>redis.DurationCmd).Result()
if ttl < 0 { /</em> 标记为离线 */ }
}
}- 每次
SCAN的count建议设为 50–200,太高易打满连接,太低轮询次数多 -
TTL返回负值(-1表示永不过期,-2表示 key 不存在)——真正离线的是-2 - 健康检查周期应略大于心跳 TTL(比如 TTL=30s,检查间隔设为 35s),避免误判
服务发现层怎么安全读取心跳状态
下游服务不应直连 Redis 查 GET service:xxx,否则会把 Redis 暴露在业务流量里。推荐加一层轻量代理:
- 用 Go 写个 HTTP 接口,如
GET /health/{service},内部走EXISTS+TTL组合判断 - 响应头加
Cache-Control: no-cache,禁用 CDN 缓存 - 对高频查询做本地缓存(比如 5s TTL 的内存 map),但必须监听 Redis 的
__keyevent@0__:expired事件及时失效
注意:KEYEVENT 默认关闭,需在 redis.conf 中显式开启 notify-keyspace-events Ex,否则缓存无法及时更新。

















