不能只用incr+expire做秒杀限流,因其非原子操作导致竞态:key过期后incr新建但未设过期、老版本expire不原子、秒级精度无法满足毫秒窗口要求;必须用lua脚本或redis_rate库保证取令牌+更新+过期全链路原子性。

秒杀场景下直接用内存限流会失效,必须依赖 Redis 做分布式计数 + 原子操作;但单纯 INCR + EXPIRE 组合在高并发下会丢请求、超阈值,得用 Lua 脚本或专用库保证原子性。
为什么不能只用 INCR + EXPIRE 做秒杀限流
看似简单:每次请求 INCR key,再 EXPIRE key 1。但实际会出三类问题:
-
EXPIRE和INCR是两个独立命令,中间存在竞态窗口——比如 key 刚过期被删,INCR创建新 key 但没设过期,后续所有请求都算在同一个 key 下,彻底失控 - Redis 2.6.12 之前
SET key 1 EX 1 NX不原子,老版本集群可能漏设过期时间 - 秒杀通常要求“精确到毫秒级窗口”,而
EXPIRE最小单位是秒,1 秒内大量请求仍可能突破库存
推荐方案:用 redis_rate 库 + AllowN 做令牌桶限流
它底层封装了 Lua 脚本,在单次 Redis 请求中完成“取令牌 + 更新剩余数 + 设置过期”,无竞态、不丢请求。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 安装:
go get github.com/go-redis/redis_rate(适配go-redis/v9) - 初始化时传入
*redis.Client和速率,例如每秒 100 个令牌:limiter := redis_rate.NewLimiter(rdb, redis_rate.PerSecond(100)) - 在 Gin 中间件里调用:
ok, resetAfter := limiter.AllowN(ctx, "seckill:"+uid, 1),其中"seckill:"+uid是带用户标识的 key,避免全量共享桶 - 注意返回的
resetAfter是剩余等待时间,前端可据此做退避重试,别只看ok布尔值
手写 Lua 实现滑动窗口限流(适合超大流量、需毫秒精度)
当秒杀 QPS 超过 5k、且要求“过去 100ms 内最多 5 次请求”时,redis_rate 的固定周期模型不够用,得用 ZSET + Lua。
- 脚本核心逻辑:用
ZADD key ts ts记录毫秒级时间戳,ZREMRANGEBYSCORE key 0 (ts-100000)清理过期项,ZCARD key获取当前窗口请求数 - 必须在
ZADD前执行ZREMRANGEBYSCORE,否则窗口数据会漂移;PEXPIREAT要配合redis.call("TIME")算毫秒级过期,不能用EXPIRE - key 命名建议包含业务维度,例如
"seckill:sku_123:ip_"+c.ClientIP(),防止不同商品或 IP 互相干扰
容易被忽略的 Redis 版本与配置细节
Redis 8.2.3 修复了 CVE-2025-62507(RCE 漏洞),但更重要的是它稳定支持 ZREMRANGEBYSCORE 在高负载下的原子性——旧版 Redis 在 zset 数据量大时可能卡顿甚至返回不完整结果。
- 生产环境务必确认 Redis 版本 ≥ 8.2.3,尤其涉及秒杀的集群节点
- 禁用
maxmemory-policy noeviction,否则限流 key 被驱逐会导致计数归零,瞬间放行超额请求 - 如果用
redis_rate,别设置过大的Period(如 1 小时),否则 Redis 中 key 会持续堆积,占用内存
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










