Predis事务异常:Redis限流器部分Key无过期时间问题求助
问题分析与解决方案
问题根源
你的代码存在两个关键问题:
- 竞态条件:在
watch之后、事务执行前,你在事务外调用exists($key)做判断,这个判断结果可能已经过时(比如Redis在这段时间内删除了过期的key,或者其他客户端修改了key),导致后续事务逻辑出错。 - 过期时间管理漏洞:仅在首次创建key时设置过期时间,后续
rpush时不更新过期时间。更严重的是,当Redis通过惰性删除机制处理过期key时,若此时执行rpush,Redis会先删除过期key再创建新key,但事务中仅包含rpush,导致新生成的key无过期时间。
Redis事务的原子性仅保证事务内命令要么全执行要么全不执行,但无法解决事务外判断与事务内执行之间的竞态问题。
解决方案:使用Redis Lua脚本
Redis Lua脚本可以保证逻辑的原子性,彻底避免竞态条件,同时统一处理rpush和过期时间设置,解决key无过期时间的问题。
Lua脚本代码
local key = KEYS[1] local limit = tonumber(ARGV[1]) local now = ARGV[2] local expiry = tonumber(ARGV[3]) -- 获取当前列表长度 local current_len = redis.call('llen', key) -- 判断是否超过限流阈值 if current_len >= limit then return 0 -- 超过限制,返回0 end -- 将当前时间戳加入列表 redis.call('rpush', key, now) -- 刷新key的过期时间(确保key始终有过期时间) redis.call('expire', key, expiry) return 1 -- 允许请求,返回1
PHP调用示例(Predis)
$luaScript = <<<LUA local key = KEYS[1] local limit = tonumber(ARGV[1]) local now = ARGV[2] local expiry = tonumber(ARGV[3]) local current_len = redis.call('llen', key) if current_len >= limit then return 0 end redis.call('rpush', key, now) redis.call('expire', key, expiry) return 1 LUA; // 执行Lua脚本,1表示KEYS数组的长度 $result = $redisClient->eval($luaScript, 1, $key, $limitNum, $now, $expiryTime); return $result == 1;
优势说明
- 原子性:Lua脚本在Redis中原子执行,彻底消除事务外判断与事务内执行的竞态问题。
- 过期时间保障:每次执行
rpush后都刷新过期时间,确保key始终有过期时间,同时让限流窗口保持滚动(比如1分钟限流,每次请求都会刷新过期时间,保证窗口是最近1分钟的请求)。 - 逻辑简化:无需使用
watch、multi等事务操作,代码更简洁可靠。
内容的提问来源于stack exchange,提问作者archer woo
相关产品推荐
相关产品推荐

