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
-- 后续逻辑...
这个脚本中的exists、hset、pexpire操作是连续执行的,中间不会有其他客户端的命令插入。
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. 事务为什么不适用
- 竞态条件:事务中的命令之间可能被其他客户端插入操作
- 无条件判断:事务中无法根据中间结果做条件分支
- 无循环逻辑:无法实现重试机制
- 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提供的最佳解决方案,而事务由于其局限性,不适合实现复杂的分布式锁逻辑。