现场是这样的。用 Redis 做每分钟 60 次限流,代码先 INCR 再 EXPIRE,发布时进程被 kill,这个 key 没有过期时间,第二天该用户一直被拒。

中间走过弯路,把「没有用 Lua 保证原子性」当成主因,改完现象没变。

把日志和配置对完,能解释这个现象的基本就这几处。两条命令之间进程退出,key 永久驻留;EXPIRE 只在第一次计数时设置;多实例时钟不一致,窗口边界漂移。

先查可疑 key 的 TTL 是不是负一

redis-cli TTL rl:user:1001

再往下改用 Lua 脚本一次完成计数和过期

redis-cli EVAL "local c=redis.call('INCR',KEYS[1]);if c==1 then redis.call('EXPIRE',KEYS[1],ARGV[1]) end;return c" 1 rl:user:1001 60

给所有限流 key 加统一前缀便于清理

python -c "print('rl:{scope}:{id}')"

补一个巡检任务扫描无过期时间的限流键

python scan_no_ttl.py --pattern 'rl:*' --fix

别只看报错消失,验证要多做一步。压测后每个限流 key 都有 60 秒左右的 TTL,进程重启不会留下永久计数,429 比例恢复正常。

这里还有个坑。计数和过期必须在一个原子操作里完成,用 SET NX EX 或 Lua 都行,分开写迟早会在某个异常路径上留下脏键。监控比日志更早给出信号,事后回看经常能提前半小时发现异常。

作者 9823189