首页/数据/数据库速查/计数器场景:限流与库存扣减
Redis 命令96 条

计数器场景:限流与库存扣减

固定窗口限流=INCR + 首次设过期;库存扣减=DECR 后判断是否 <0 再回补;更精细的窗口用 Lua 或 Redis Cell 模块。

数据库速查数据更新于 2026-10-13

计数器场景是什么

限流与库存扣减都建立在计数器的原子自增自减之上。固定窗口限流=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 做过期率巡检。

易错点

  • 计数 key 忘记设过期,用户一次超限后被永久拒绝
  • 先 GET 后 SET 实现库存扣减,并发下扣成负数造成超卖
  • EXPIRE 不带 NX 反复调用,把窗口不断向后推移导致限流永不触发
  • 把限流放在应用本地的内存计数里,多实例部署后总量被放大数倍
  • 限流规则变更后旧 key 未清理,新老规则叠加导致误杀

数据来源与参考

  • Redis 官方文档: INCR
  • Redis 官方文档: EVAL
  • Redis 官方文档: 使用模式(限流)

相关工具

​‌‌​​‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌‌​​‌‌​​‌​‌‌‌​​‌‌‌​​​‌​‌‌​‌​​‌​‌‌‌​​​‌​‌‌​‌​​‌​‌‌​​​‌‌​‌‌‌​‌​​​‌‌​‌‌‌‌​​‌​‌‌‌​​‌‌​​​‌‌​‌‌​‌‌‌‌​‌‌​‌‌​‌