直接用 redis.SetNX 防击穿会失败,因其仅原子写入、无所有权校验与自动续期,高并发下多个请求仍会重复查库写缓存;需结合 singleflight.Group(主)与 Redis 锁(兜底)双保险,并严格绑定 context 生命周期、用 Lua 安全删锁、Go 层埋点监控。

为什么 Echo 里直接用 redis.SetNX 防击穿会失败
因为 SetNX 只是原子写入,不带所有权校验和自动续期,而缓存击穿发生在高并发读+单 key 过期瞬间——此时多个请求几乎同时执行 SetNX,但只有第一个成功,其余仍会各自查 DB、各自写缓存,根本没起到“只放行一次回源”的作用。
更糟的是,如果业务处理慢(比如 DB 查询 >5s),而锁 TTL 设成 3s,第二个请求在第一个还没写完缓存时就抢到了锁,导致重复加载;或者锁被误删(Del 不校验 value),让第三个请求也挤进来。
所以不能把 SetNX 当防击穿开关用,它只是底层原语,必须包裹在有状态、有生命周期控制的封装里。
用 singleflight.Group + Redis 锁做双保险最稳妥
Go 生态里真正解决击穿问题的主力不是分布式锁,而是 singleflight.Group。它天然适合 Echo 的 handler 模型:每个请求按 key 合并,只让一个 goroutine 执行加载逻辑,其余等待返回结果。
立即学习“go语言免费学习笔记(深入)”;
-
singleflight.Group必须全局复用,别在 handler 里new——否则每次新建都失去去重能力 - key 必须稳定可比,推荐
fmt.Sprintf("cache:product:%d", id),别用id原值(int 类型 hash 不一致) - 真实回源逻辑(查 DB + 写 Redis)必须完整包进
Do()的func() (interface{}, error)里,不能在外面先查 DB 再传结果 - 搭配 Redis 锁仅用于兜底:比如
singleflight因 panic 或 context cancel 失效时,用SET product:123_lock "req-xxx" NX EX 10防止雪崩式打库
在 Echo middleware 中注入锁上下文要避开 goroutine 泄漏
有人想在中间件里统一加锁,比如拦截所有 /api/product/:id 请求并自动抢锁。这容易踩两个坑:锁对象生命周期失控、续期协程逃逸。
典型错误是启动一个后台 goroutine 续期,但没绑定到 request context,导致 handler 返回后续期还在跑,最终续的是下一个请求的锁。
- 锁实例必须绑定到当前请求的
context.Context,用context.WithCancel控制生命周期 - 续期协程启动前,检查
ctx.Err() != nil,并在 defer 里显式 stop - 别在 middleware 里直接调
go renewLock(...),应封装成可取消的结构体方法,例如l := NewRedisLock(rdb, key); l.Acquire(ctx); defer l.Release(ctx) - 释放锁必须用 Lua 脚本:
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 product:123_lock req-xxx,否则GET + DEL有竞态
监控和降级点必须落在 Go 层,不在 Redis 配置里
击穿是否发生,不能靠 Redis 慢日志或 INFO stats 判断。真正信号在 Go 层:比如 singleflight.Do 返回的 shared 字段为 false 的次数突增,或 rdb.Get(ctx, key).Err() 频繁返回 redis.Nil 但后续没触发回源。
这些指标得在 Echo 的 handler 里埋点,而不是等 Prometheus 抓 Redis 指标。
- 务必用
errors.Is(err, redis.Nil)判断空值,err == redis.Nil永远为 false(它是变量,不是常量) - 对
redis.Nil要走正常回源流程;对context.DeadlineExceeded或网络错误才该降级返回旧缓存或默认值 - 锁获取失败(如
SETNX返回 false)不应立即返回错误,而应 fallback 到singleflight.Do,形成双路径保障
复杂点不在加锁动作本身,而在锁与请求生命周期、panic 恢复、context 取消之间的耦合——少一处 defer 或多一个裸 go,线上就可能偶发重复写库。


















