setIfAbsent 不够安全,因其无法原子性设置过期时间,易导致锁永久不释放;Redis 推荐用 SET key value NX EX seconds 一步完成,但 Spring Data Redis 老版本 setIfAbsent 不支持 EX 参数。

直接用 setIfAbsent 为什么不够安全?
因为 setIfAbsent 只能保证「设置键不存在时才写入」,但无法原子性地绑定过期时间——如果先设值再 expire,中间崩溃或网络中断,锁就永远不释放。Redis 官方推荐用 SET key value NX EX seconds 命令一步到位,但 Spring Data Redis 的 setIfAbsent 方法(尤其老版本)默认不支持传 EX 参数,容易漏掉这个关键约束。
用 Lua 脚本实现带自动过期的互斥锁
Lua 在 Redis 中是原子执行的,适合封装「判断+设置+过期」逻辑。核心脚本就两行,但必须严格校验参数:
if redis.call('exists', KEYS[1]) == 0 then
return redis.call('setex', KEYS[1], tonumber(ARGV[1]), ARGV[2])
else
return 0
end
调用时注意:
-
KEYS[1]是锁名(如"order:123"),必须唯一且可预测 -
ARGV[1]是过期秒数(如"30"),别传 0 或负数,否则锁永不过期 -
ARGV[2]是随机 value(如"8f4a2c7e"),用于后续解锁校验,不能硬编码 - Spring 中用
redisTemplate.execute()执行,别用opsForValue().setIfAbsent()包一层再调 Lua——多一次 round-trip 就破坏原子性
解锁必须用 Lua 校验 value 再删,不能直接 del
直接 DEL 键会误删别人持有的锁。正确方式是用 Lua 先比对 value 是否匹配,再删:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
常见翻车点:
- 解锁时传错
value(比如用了固定字符串而不是加锁时生成的随机串)→ 脚本返回 0,锁残留 - 用
redisTemplate.delete()替代 Lua → 拿到锁的线程崩溃后,其他线程可能删掉别人的锁 - 没处理 Lua 返回值:返回
1表示成功,0表示失败,必须检查,否则以为解锁成功
超时时间选多少?不是越长越安全
过期时间本质是「最大持有时间」,设太长会导致故障时锁堆积;设太短又可能业务没做完锁就失效。实际取值要满足:业务平均耗时 × 2 ~ 3,且必须配合主动续期(如看门狗)或幂等重试。例如下单接口平均 800ms,可设 3000 毫秒(3 秒),但得确保最慢路径(含 DB、RPC)不超过这个阈值。另外,SET 命令里的 EX 是秒级,毫秒级要用 PX,Lua 里对应 psetex。
真正难的是锁的可重入性、阻塞等待、公平性——这些不用 Redisson 就得自己靠 value 拼接线程 ID + 计数器 + 队列 Key 来模拟,一不小心就死锁或漏解锁。Lua 脚本能保原子性,但编排逻辑还是得自己兜底。

















