计数器场景是什么
限流与库存扣减都建立在计数器的原子自增自减之上。固定窗口限流=INCR 一次并在首次设置过期时间;库存扣减=DECR 后判断是否小于 0,超卖时再回补。关键点是判断与修改必须在同一条命令或一段 Lua 里完成,先 GET 再 SET 的写法在并发下必然不准。
基本信息
| 核心命令 | INCR / DECR / INCRBY / DECRBY,单命令原子 |
|---|---|
| 过期设置 | EXPIRE 加 NX 参数只在未设过期时生效 |
| 限流粒度 | 按用户、接口、分钟组合成 key |
| 库存模式 | 预扣加超时回补,或 Lua 内判断后扣减 |
| 精确限流 | 滑动窗口用 ZSET 或 Lua,令牌桶可用 Redis Cell 模块 |
典型使用场景
- 接口限流:每个用户每分钟 60 次,计数超限直接拒绝
- 库存防超卖:扣减与判断在原子脚本内完成,避免负库存
- 活动防刷:按设备或 IP 统计单位时间内的请求次数
- 告警阈值统计:短时间内错误数累计到阈值即触发告警
- 额度控制:按账户维度做每日配额计数并在零点自然过期
- 分布式协调统计:多实例共享同一计数器,替代本地内存计数
示例
示例
INCR rate:api:u1001:202610121030 # 第 1 次返回 1
EXPIRE rate:api:u1001:202610121030 60 NX # NX:已设过期则不重置
GET rate:api:u1001:202610121030 # 第 61 次触发限流
# 更稳的原子写法:
EVAL "local c=redis.call('INCR',KEYS[1]) if c==1 then redis.call('EXPIRE',KEYS[1],ARGV[1]) end return c" 1 rate:key 60
SET stock:sku88 100
DECR stock:sku88 # 返回 99
EVAL "local s=tonumber(redis.call('GET',KEYS[1]) or '0') if s<=0 then return -1 end return redis.call('DECR',KEYS[1])" 1 stock:sku88限流方案对照
| 方案 | 实现要点 | 局限 |
|---|---|---|
| 固定窗口 | INCR 加 EXPIRE,实现最简单 | 窗口切换瞬间可能放过双倍流量 |
| 滑动窗口 | ZSET 存时间戳,按区间统计 | 内存与耗时随请求量增长 |
| 令牌桶 | Lua 计算补充速率与余量 | 脚本逻辑复杂,需充分测试 |
| 库存预扣 | DECR 判断后回补或 Lua 原子扣减 | 回补失败会留下少卖,要有对账 |
先 GET 再 SET 的库存写法在并发下必超卖;判断与扣减必须在 Lua 或单命令里原子完成。
限流计数器没设过期时间是常见事故:一个 key 的计数只增不减,用户被永久限流;用 EXPIRE NX 或 Lua 内首次计数时设过期都可避免,同时要对这类 key 做过期率巡检。