秒杀场景下必须使用Redis ClusterClient而非单节点Client,因其支持自动分片、故障转移和拓扑发现;库存扣减须用Lua脚本保证原子性,避免竞态超卖;限流应前置至Gin中间件层,采用分桶令牌桶实现fail-fast;缓存一致性依赖删缓存+空值防穿透,而非更新缓存。

秒杀场景下 Redis 连接必须用 ClusterClient 而不是 Client
单节点 redis.Client 在秒杀时扛不住流量洪峰,且无法自动分片和故障转移。Gin 服务直连单 Redis 实例,一旦该实例 CPU 打满或网络抖动,整个秒杀接口就雪崩——这不是理论风险,是线上真实发生的故障模式。
必须用 redis.NewClusterClient,传入至少 3 个 seed node 地址(如 []string{"redis-0:7000", "redis-1:7000", "redis-2:7000"}),由客户端自动发现拓扑、路由 key、重试失败节点。别指望手动轮询或哨兵兜底,秒杀流量下哨兵切换延迟不可控。
-
MaxRedirects至少设为 8,避免重定向链过长导致超时 - 所有节点地址必须可被容器网络互通,不能混用 localhost 和内网 DNS
- 集群模式下
SET不支持EX和NX同时用,得改用 Lua 脚本保证原子性
库存扣减必须用 Lua 脚本而非 SETNX + GET
用 client.SetNX 判断库存再 client.Get 读值,中间存在竞态窗口:两个请求同时通过 SetNX,都去查 DB 或扣库存,超卖就发生了。秒杀场景下这个窗口哪怕只有几微秒,QPS 上万时也必现。
正确做法是把“判断 + 扣减 + 设置新值”三步压进一个 Lua 脚本里执行,Redis 保证原子性。脚本返回 1 表示扣成功,0 表示库存不足或 key 不存在。
立即学习“go语言免费学习笔记(深入)”;
local stock = redis.call("GET", KEYS[1])
if not stock then
return 0
end
local stock_int = tonumber(stock)
if stock_int <= 0 then
return 0
end
redis.call("SET", KEYS[1], stock_int - 1)
return 1- 脚本里别用
redis.call("DECR"),它不检查负数,库存可能被扣成 -1 - KEYS[1] 必须是带业务前缀的完整 key,比如
"seckill:sku:10086" - Gin handler 中调用
client.Eval(ctx, script, []string{key}, nil).Result(),务必带ctx超时
限流与排队要在 Gin 中间件层做,别甩给 Redis
把所有请求都打到 Redis 再用 INCR + EXPIRE 做限流,等于把压力全引向 Redis。秒杀开始瞬间,Redis 的 CPU 会先于你的 Go 服务被打满,连接池直接耗尽,redis: connection pool timeout 错误刷屏。
限流逻辑必须前置:在 Gin 的 middleware 里用 golang.org/x/time/rate.Limiter 做令牌桶,每秒放行固定数量请求;超出的直接 c.AbortWithStatusJSON(429, ...) 返回,连 Redis 都不碰。
- rate.NewLimiter(rate.Every(time.Second/100), 100) —— 每秒最多 100 请求,burst 设为 100
- 不要用全局 limiter,按商品 ID 分桶:
limiterMap.LoadOrStore(skuID, newLimiter()) - 排队队列用内存 channel 或 ring buffer,别存 Redis;channel 容量设为预估峰值并发的 1.5 倍,满则拒绝
缓存一致性靠“删缓存”而非“更新缓存”
秒杀成功后,如果走 client.Set 更新库存缓存,一旦写缓存失败(网络抖动、Redis 拒绝连接),缓存值就永久脏了。而用户下次查库存看到的是旧值,以为还能抢,实际已售罄。
正确姿势是:DB 扣减成功后,立刻 client.Del(ctx, key) 删除缓存。下次请求未命中,自然回源查 DB 并重建缓存——只要 DB 是强一致的,缓存最终一定对。
- 删除操作必须加重试:失败时投递到异步任务队列(如 Kafka 或内存 channel),最多重试 3 次
- 别用
SET+EX给缓存设固定 TTL,秒杀期间库存变化频繁,TTL 会导致缓存滞后 - DB 回源查询时,记得用空值缓存防穿透:
client.Set(ctx, key, "null", 30*time.Second)
秒杀不是堆参数能解决的,关键路径上每个环节都要有明确的 fail-fast 策略:限流卡在最外层,Lua 卡在原子性,删缓存卡在最终一致性。任何试图“先写缓存再同步 DB”或“全量依赖 Redis 原子命令”的设计,在真实秒杀流量下都会暴露精度或性能缺陷。


















