EXPIRE无法原子续期多个key,因单命令仅支持一个key且网络异常会导致错期;必须用Lua脚本通过EVAL原子执行PEXPIRE批量续期,确保一致性。

为什么 EXPIRE 不能直接用于多个 Key 的原子续期
Redis 单个 EXPIRE 命令只能操作一个 key,而网络往返、客户端重试、中间失败都会导致多个 key 的过期时间不同步。比如你先对 order:123 续期,再对 order:123:lock 续期,中间若断连或超时,两个 key 就会“错期”——一个还活着,一个已过期,业务逻辑就可能出乱子。
真正可靠的同步续期,必须靠服务端原子执行。Lua 脚本是唯一能保证多 key 操作在单次 Redis 执行中完成的方式。
用 EVAL 执行 Lua 脚本批量续期的写法
核心思路:把要续期的 key 列表和新 TTL 作为参数传入,脚本里遍历并统一调用 PEXPIRE(推荐用毫秒级,精度更高、避免秒级截断误差)。
EVAL "for i=1,#KEYS do redis.call('PEXPIRE', KEYS[i], ARGV[1]) end" 2 order:123 order:123:lock 30000-
KEYS是 key 列表,ARGV[1]是统一的新过期毫秒数(如30000表示 30 秒) - 注意:第一个数字
2是 key 的数量,必须和实际传入的 key 个数一致,否则报错ERR Error running script (call to f_...): @user_script:1: WRONGTYPE Operation against a key holding the wrong kind of value - 如果某个 key 不存在,
PEXPIRE返回 0,不影响其他 key,脚本继续执行
续期前要不要先检查 key 是否存在?
大多数场景下不用。续期动作本身是“保活”,不是“创建”。如果 key 已被删,PEXPIRE 返回 0,脚本照常结束,客户端可据此判断是否需重建资源。强行加 EXISTS 判断反而增加复杂度,且无法规避“检查后被删”的竞态。
但有例外:如果你依赖 key 存在作为业务状态标识(比如 user:456:session 必须存在才允许续期),那就得在脚本里显式判断:
EVAL "for i=1,#KEYS do if redis.call('EXISTS', KEYS[i]) == 0 then return 0 end end for i=1,#KEYS do redis.call('PEXPIRE', KEYS[i], ARGV[1]) end return 1" 2 user:456:session user:456:token 60000
这个版本返回 0 表示任一 key 缺失,1 表示全部续期成功。
本地缓存 + Lua 续期组合使用时的坑
常见错误是客户端一边在本地缓存了 key 的过期时间,一边又调 Lua 续期,但没同步更新本地时间戳,导致后续误判 key 过期。
- 不要在客户端维护“预计过期时间”,只维护“最后续期时间 + TTL”,每次续期后立刻刷新这个时间点
- Lua 脚本无法获取 Redis 服务器当前毫秒时间(
redis.call('TIME')返回的是秒+微秒,不精确),所以别指望脚本里算“剩余时间”再决定是否续期 - 如果业务要求“只有剩余不足 5 秒才续期”,必须由客户端判断,然后决定是否发 Lua 请求;Lua 只负责“无条件续期”
最简稳的模式就是:定时轮询触发 Lua 续期,不查状态,不看剩余,只管刷 TTL。复杂判断逻辑留在应用层,别塞进 Lua。

















