分布式锁的实现
Redisson 最出名的功能之一是分布式锁(RLock)。它的锁机制基于 Redis 的原子性操作:
- 使用 SET NX(SET if Not eXists)命令尝试获取锁,并设置一个过期时间(防止死锁)。
- 通过 Lua 脚本确保锁的释放是原子性的,只有持有锁的客户端才能释放锁。
- 支持锁续期(Watchdog 机制):如果任务未完成,Redisson 会自动为锁延长过期时间。
实现细节
1. 锁的基本存储
Redisson 使用 Redis 的 Hash 数据结构来存储锁的状态。锁的键名默认是用户指定的锁名称(例如 myLock),而 Hash 中的字段(field)则是客户端的唯一标识符(通常是线程 ID 或 UUID),值则是锁的重入计数。
- Redis 中的表现形式:
这里 myLock 是锁的键名,thread-id-123 是客户端标识,1 表示锁被获取了一次(支持重入)。myLock: { "thread-id-123": 1 }
2. 获取锁的流程
Redisson 获取锁时,依赖 Redis 的 SET NX(SET if Not eXists)或更复杂的 Lua 脚本。以下是详细步骤:
- 尝试加锁: Redisson 执行一个 Lua 脚本,脚本逻辑大致如下:
- 检查锁是否存在(通过 HEXISTS 检查 Hash 中是否有该客户端的字段)。
- 如果锁不存在,使用 HSET 设置锁状态,并通过 PEXPIRE 设置过期时间(默认 30 秒)。
- 如果锁已存在但属于当前客户端(重入),增加重入计数(HINCRBY)。
- 如果锁被其他客户端持有,返回失败。
- Lua 脚本的优势: Redis 执行 Lua 脚本是原子性的,避免了多线程并发时的竞争问题。脚本伪代码可能像这样:
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hset', KEYS[1], ARGV[1], 1);
redis.call('pexpire', KEYS[1], ARGV[2]);
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[1]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[1], 1);
redis.call('pexpire', KEYS[1], ARGV[2]);
return nil;
end;
return redis.call('pttl', KEYS[1]);
-
- KEYS[1]:锁的键名。
- ARGV[1]:客户端标识。
- ARGV[2]:锁的过期时间(毫秒)。
- 返回结果: 如果成功获取锁,返回 nil;如果失败,返回锁的剩余存活时间(pttl),客户端可以据此决定是否重试。
4. 释放锁的流程
释放锁同样通过 Lua 脚本实现,确保只有锁的持有者能释放锁:
if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
return nil;
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1);
if (counter > 0) then
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
end;
redis.call('del', KEYS[1]);
redis.call('publish', 'redisson_lock_channel', ARGV[3]);
return 1;
释放逻辑:
- 检查锁是否存在且属于当前客户端(HEXISTS)。
- 如果是重入锁,减少计数(HINCRBY -1)。
- 如果计数降为 0,删除锁(DEL)。
- 如果锁不属于当前客户端,返回失败。
关键特性总结
- 高可用性:在 Redis 集群中,锁状态会同步到多个节点。
- 原子性:通过 Lua 脚本保证操作的原子性。
- 重入性:支持同一客户端多次获取锁,使用计数器管理。
安全性:只有锁的持有者能释放锁。