Redisson分布式锁为什么要用lua脚本实现,而不用事务?

Redisson使用Lua脚本实现分布式锁主要是为了解决原子性、网络开销和事务局限性三个核心问题。下面详细解释:

1. 原子性保证

这是最主要的原因。Lua脚本在Redis中执行时是原子性的,整个脚本在执行过程中不会被其他命令打断。

-- Redisson的加锁Lua脚本示例
if (redis.call('exists', KEYS[1]) == 0) then
    redis.call('hset', KEYS[1], ARGV[2], 1)
    redis.call('pexpire', KEYS[1], ARGV[1])
    return nil
end
-- 后续逻辑...

这个脚本中的existshsetpexpire操作是连续执行的,中间不会有其他客户端的命令插入。

2. Redis事务的局限性

对比项Redis事务Lua脚本
原子性伪原子性(命令间可插入其他操作)真原子性
中间结果不可见可访问中间计算结果
条件判断支持复杂条件逻辑
回滚机制不支持(无真正的rollback)脚本失败则全不执行

Redis事务(MULTI/EXEC)的问题:

# 客户端A
MULTI
WATCH lock_key
GET lock_key
# 此时客户端B可以修改lock_key
SET lock_key value
EXEC  # 如果watch的key被修改,事务失败

3. 复杂操作的需求

分布式锁需要多个操作的组合:

-- 完整流程包含:
-- 1. 检查锁状态
-- 2. 获取锁(设置值)
-- 3. 设置过期时间
-- 4. 处理重入锁逻辑
-- 5. 返回TTL等

这些操作如果用普通命令或事务,需要多次网络往返,而Lua脚本一次完成。

4. 网络开销优化

// 不用Lua脚本的伪代码(问题示例)
if (!redis.exists("lock")) {        // 第一次网络往返
    redis.set("lock", "value");     // 第二次网络往返
    redis.expire("lock", 10);       // 第三次网络往返
}
// 在exists和set之间,其他客户端可能插入获取锁

5. Redisson的具体实现

Redisson的锁实现包含多种Lua脚本:

加锁脚本

-- 检查锁是否存在,不存在则获取
-- 处理可重入逻辑
-- 设置过期时间
-- 返回锁的TTL

解锁脚本

-- 检查是否是当前线程持有锁
-- 减少重入计数
-- 计数为0时删除锁
-- 发布解锁消息

续期脚本

-- 检查锁是否仍被当前线程持有
-- 重置过期时间

6. 事务为什么不适用

  1. 竞态条件:事务中的命令之间可能被其他客户端插入操作
  2. 无条件判断:事务中无法根据中间结果做条件分支
  3. 无循环逻辑:无法实现重试机制
  4. WATCH的局限:虽然WATCH可监测键变化,但无法实现复杂的锁逻辑

实际应用对比

使用Lua脚本

-- 原子执行,无竞态条件
local result = redis.call('setnx', KEYS[1], ARGV[1])
if result == 1 then
    redis.call('expire', KEYS[1], ARGV[2])
end
return result

使用事务的问题

# 客户端1
WATCH mylock
GET mylock
# 此时客户端2可能获取锁
MULTI
SET mylock value
EXEC  # 可能因为WATCH失效而失败

总结

Redisson选择Lua脚本的主要原因:

原因说明
原子性脚本整体原子执行,避免竞态条件
性能减少网络往返次数
复杂性支持条件判断、循环等复杂逻辑
一致性确保获取锁、设置过期时间等操作的原子性
可维护性脚本逻辑集中,便于理解和维护

对于分布式锁这种对原子性要求极高的场景,Lua脚本是Redis提供的最佳解决方案,而事务由于其局限性,不适合实现复杂的分布式锁逻辑。


作 者:南烛
链 接:https://www.itnotes.top/archives/1061
来 源:IT笔记
文章版权归作者所有,转载请注明出处!


上一篇
下一篇