不能直接用 incr+expire 因存在竞态窗口导致 key 永久不过期或过期时间被覆盖;需用 lua 脚本原子执行 exists/set/expire 或 incrby/expire,并确保 ttl 参数为纯数字字符串。

为什么不能直接用 INCR + EXPIRE 两次命令
因为 Redis 的 INCR 和 EXPIRE 是两个独立命令,中间存在竞态窗口:如果客户端执行完 INCR 后崩溃或网络中断,EXPIRE 就不会被执行,导致 key 永久存在且无过期时间。分布式环境下多个客户端并发时,这个问题会放大——比如两个请求同时读到旧值、各自自增、再各自设过期,最终结果错乱且过期时间被反复覆盖。
用 EVAL 执行 Lua 脚本保证原子性
Redis 对 Lua 脚本的执行是原子的:脚本内所有命令按顺序执行,期间不会有其他命令插入。关键点在于,你要在脚本里手动处理“首次设置 + 过期”和“后续自增”两种情况:
- 先用
GET检查 key 是否存在;不存在则用SET设置初始值并带EX参数(注意不是EXPIRE) - 存在则用
INCR;但此时不能直接EXPIRE(因为EXPIRE对已存在的 key 有效,但会重置过期时间,可能干扰业务语义) - 更稳妥的做法是:统一用
SET key value EX seconds NX初始化,失败则INCR,再统一续期(见下一点)
实际常用写法(推荐):
if redis.call("exists", KEYS[1]) == 0 then
return redis.call("set", KEYS[1], ARGV[1], "EX", ARGV[2])
else
local ret = redis.call("incr", KEYS[1])
redis.call("expire", KEYS[1], ARGV[2]) -- 续期,确保每次操作后都刷新 TTL
return ret
end
注意 ARGV[2] 必须是字符串数字,且不能为负
Redis Lua 中 EX 和 expire 接收的都是字符串形式的整数秒数。如果你传入浮点数(如 30.5)或非数字字符串(如 "30s"),会报错 ERR value is not an integer or out of range。调用时务必确保:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
ARGV[2]是纯数字字符串,例如"60",不是60(数字类型在部分客户端会被自动转成浮点) - 使用
redis-cli --eval时,参数之间用空格分隔,不要加引号包裹数字 - 在 Python 中用
redis-py时,传args=[init_val, str(ttl_sec)],别传int
INCRBY 与初始化值不一致时的坑
上面脚本默认初始化值为 ARGV[1],但 INCR 每次加 1。如果你需要支持任意步长(比如每次 +5),就不能只靠 INCR。正确做法是改用 INCRBY 并把步长也作为参数传入:
local step = tonumber(ARGV[3]) or 1
if redis.call("exists", KEYS[1]) == 0 then
return redis.call("set", KEYS[1], ARGV[1], "EX", ARGV[2])
else
local ret = redis.call("incrby", KEYS[1], step)
redis.call("expire", KEYS[1], ARGV[2])
return ret
end
这时调用需传三个 ARGV:初始值、TTL 秒数、步长。漏传或类型错都会让 tonumber() 返回 nil,导致 INCRBY 报错 ERR value is not an integer。
真正容易被忽略的是:即使脚本原子,也不能解决“多个不同 TTL 需求混用”的问题——比如 A 服务希望 key 过期 60 秒,B 服务希望 300 秒,它们共用一个 key 时,expire 调用会互相覆盖。这种场景必须拆 key 或引入外部协调逻辑。










