
本文介绍如何利用 redis 的 lua 脚本原子性,安全地实现“仅在键已存在前提下更新其值”的逻辑,彻底规避竞态条件(如过期瞬间的检查-执行时间差),确保业务逻辑强一致性。
本文介绍如何利用 redis 的 lua 脚本原子性,安全地实现“仅在键已存在前提下更新其值”的逻辑,彻底规避竞态条件(如过期瞬间的检查-执行时间差),确保业务逻辑强一致性。
在 Redis 中,原生命令如 SET、SETNX 或 EXISTS + SET 组合均无法真正保证“键存在才写入”的原子语义——因为两次命令之间存在毫秒级时间窗口,键可能在检查后立即过期或被删除,导致后续 SET 失效或意外创建新键。这在定时刷新类场景(如每日库存 store_of_today)中尤为危险:若操作跨越零点,极小概率下 GET 返回非空值,但 SET 执行前键已过期,最终写入一个无过期时间的脏数据。
根本解法:使用 Lua 脚本封装原子逻辑
Redis 保证单个 EVAL 命令内所有操作以原子方式执行(同一连接、不可中断、无并发干扰)。我们可将“校验存在性 + 条件写入 + 续期”封装为一个 Lua 脚本,彻底消除竞态:
-- set_if_exists.lua:仅当 key 存在时设置新值并续期(若不存在则不操作)
local key = KEYS[1]
local value = ARGV[1]
local expire_at = ARGV[2]
-- 检查 key 是否存在(注意:redis.call('EXISTS', key) 返回 1/0;redis.call('GET', key) 返回 nil/false)
if redis.call('EXISTS', key) == 1 then
redis.call('SET', key, value)
redis.call('EXPIREAT', key, expire_at)
return 1 -- 成功
else
return 0 -- 键不存在,拒绝写入
end
对应 PHP 调用示例:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$key = "store_of_today";
$new_left = 85;
$expired_at = strtotime('tomorrow 00:00:00'); // 精确续期至明日零点
// 原子执行:仅当 key 存在时更新值和过期时间
$result = $redis->eval(
'local key = KEYS[1] local value = ARGV[1] local expire_at = ARGV[2] if redis.call("EXISTS", key) == 1 then redis.call("SET", key, value) redis.call("EXPIREAT", key, expire_at) return 1 else return 0 end',
[$key, $new_left, $expired_at],
1
);
if ($result === 0) {
throw new RuntimeException("Key '{$key}' does not exist, update rejected.");
}
关键设计说明:
- ✅ 原子性保障:EXISTS、SET、EXPIREAT 在单次 Lua 调用中顺序执行,无中间状态暴露;
- ✅ 精确语义:严格区分“键存在”与“键存在且非空”,避免 GET 返回 nil 导致误判(EXISTS 是唯一可靠判断依据);
- ✅ 幂等续期:每次成功更新均重置过期时间,防止因系统延迟导致键提前失效;
- ⚠️ 注意事项:Lua 脚本中禁止调用阻塞命令(如 KEYS)、避免复杂循环(影响性能),且 EXPIREAT 时间戳必须为 Unix 秒级整数(非毫秒);
- ? 补充策略:若需“键不存在则初始化”,应拆分为独立初始化脚本(如文中的 $get_lua),与更新脚本分离,职责清晰。
通过 Lua 原子脚本,我们绕过了 Redis 原生命令的语义局限,以可控、可复用的方式实现了业务所需的强一致性写入约束——这是分布式环境下保障数据正确性的关键实践。










