
本文介绍使用 Redis Lua 脚本实现键存在性校验与原子写入的可靠方案,解决高并发或临界时间窗口下因 TTL 过期导致的 SET 失败问题,避免竞态条件。
本文介绍使用 redis lua 脚本实现键存在性校验与原子写入的可靠方案,解决高并发或临界时间窗口下因 ttl 过期导致的 `set` 失败问题,避免竞态条件。
在 Redis 中,原生命令如 SET、SETNX 或 EXPIREAT 均为单命令原子操作,但多个命令组合(如先 GET 再 SET + EXPIREAT)无法保证原子性——这正是用户遇到的核心问题:在获取键值后、执行写入前,键可能因过期而被自动删除,导致后续 SET 失效,业务逻辑中断。
Redis 本身不提供 SETIFEXIST 这类复合命令,但其内置的 Lua 执行引擎提供了完美的解决方案:Lua 脚本在服务端以原子方式执行,全程无上下文切换,彻底规避竞态。
✅ 推荐方案:原子化 Lua 脚本
以下两个 Lua 脚本分别处理「初始化键」和「安全更新键」两个关键阶段:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
1. 初始化脚本(get_or_init.lua)
首次访问时检查键是否存在,不存在则初始化并设过期时间:
local key = KEYS[1]
local expireAt = tonumber(ARGV[1])
local value = redis.call('GET', key)
if value == false then
redis.call('SET', key, ARGV[2]) -- 默认值,如 100
redis.call('EXPIREAT', key, expireAt)
return ARGV[2]
end
return value2. 安全更新脚本(set_if_exists.lua)
严格确保仅当键当前存在时才更新值与过期时间(注意:EXPIREAT 若传入历史时间戳,Redis 会立即删除该键):
local key = KEYS[1]
local newValue = ARGV[1]
local newExpireAt = tonumber(ARGV[2])
-- 先检查键是否存在(非空且未过期)
if redis.call('EXISTS', key) == 1 then
redis.call('SET', key, newValue)
redis.call('EXPIREAT', key, newExpireAt)
return 1 -- 成功
else
return 0 -- 键不存在,拒绝写入
end? PHP 使用示例(基于 phpredis)
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$todayEnd = strtotime('today 23:59:59'); // 精确到秒,避免毫秒级漂移
$key = 'store_of_today';
// 阶段一:获取或初始化
$initScript = <<<LUA
local key = KEYS[1]
local expireAt = tonumber(ARGV[1])
local defaultValue = ARGV[2]
local value = redis.call('GET', key)
if value == false then
redis.call('SET', key, defaultValue)
redis.call('EXPIREAT', key, expireAt)
return defaultValue
end
return value
LUA;
$left = $redis->eval($initScript, [$key, $todayEnd, '100'], 1);
// ... 业务逻辑处理(如扣减库存)...
$newLeft = max(0, $left - 1);
// 阶段二:原子更新(仅当键仍存在)
$updateScript = <<<LUA
local key = KEYS[1]
local newValue = ARGV[1]
local newExpireAt = tonumber(ARGV[2])
if redis.call('EXISTS', key) == 1 then
redis.call('SET', key, newValue)
redis.call('EXPIREAT', key, newExpireAt)
return 1
else
return 0
end
LUA;
$result = $redis->eval($updateScript, [$key, $newLeft, $todayEnd], 1);
if ($result === 0) {
throw new RuntimeException("Key '$key' disappeared before update — handle fallback (e.g., re-init or error)");
}⚠️ 关键注意事项
- Lua 中 EXISTS 是唯一可靠的存在性判断:GET 返回 false 可能是键不存在 或 值为字符串 "false",而 EXISTS 明确返回 0/1。
- EXPIREAT 时间戳必须为整数秒:PHP 的 strtotime() 返回秒级时间戳,直接传入即可;切勿传入毫秒或浮点数。
- 避免脚本超时:Redis 默认 lua-time-limit=5000(ms),复杂逻辑需精简;本例脚本极轻量,无性能风险。
- 错误兜底不可省略:eval 返回 0 表示键已丢失,此时应触发重初始化或告警,而非静默失败。
通过将存在性校验与写入操作封装于单次 Lua 调用,我们完全消除了网络延迟、调度间隙与 TTL 边界竞争带来的不确定性——这是构建高可靠 Redis 应用的基石实践。

















