现场是这样的。用 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 都行,分开写迟早会在某个异常路径上留下脏键。监控比日志更早给出信号,事后回看经常能提前半小时发现异常。
