持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第19天,点击查看活动详情
一、前言
Redisson
是 Redis
最知名的一个开源客户端框架,学习其:官方文档
- 如何实现
Redis
分布式锁? - 各种分布式锁的原理
代码栗子如下:
// 尝试获取 myLock 这个key的锁
RLock lock = redisson.getLock("myLock");
// 常见加锁方式一:
// - 如果没有人加这把锁,那自己就可以加这把锁
// - 如果有人加这把锁了,那自己这时就会阻塞住
lock.lock();
// 常见加锁方式二:
// 尝试加锁,加锁成功后,10秒后自动释放锁
lock.lock(10, TimeUnit.SECONDS);
// 常见加锁方式三:
// 尝试加锁指定一个时间,最多等待 100秒:加锁成功返回 true,加锁失败返回 false
// 加锁成功后,10秒后自动释放锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
// 释放锁
lock.unlock();
}
}
Tips:
看门狗(watchdog
)
为避免获取锁的
Redisson
实例客户端崩溃,导致锁无法释放。 看门狗,它会在锁持有者Redisson
实例处于活动状态时延长锁的过期时间。默认锁定看门狗超时为 30 秒
上面是同步锁,现在看下异步锁:
RLock lock = redisson.getLock("myLock");
// 用其他的线程去进行加锁,不会阻塞当前的主线程运行
RFuture<Void> lockFuture = lock.lockAsync();
RFuture<Void> lockFuture = lock.lockAsync(10, TimeUnit.SECONDS);
RFuture<Boolean> lockFuture = lock.tryLockAsync(100, 10, TimeUnit.SECONDS);
lockFuture.whenComplete((res, exception) -> {
// ...
lock.unlockAsync();
});
Tips:
用哪个线程去加分布式锁,就必须用那个线程来对分布式锁进行释放。
如果用不同的线程,会导致
IllegalStateException
异常。
二、可重入锁源码剖析
(1)可重入锁源码剖析之 lua
脚本
- 获取锁实例,从这个入手:
RLock lock = redisson.getLock("myLock");
进入 getLock
方法:
// Redisson.java
@Override
public RLock getLock(String name) {
// commandExecutor 命令执行器
// 封装了跟 redis 进行通信的 Connection 连接对象
return new RedissonLock(commandExecutor, name, id);
}
- 加锁
lock.lock();
进入 lock()
方法:
// RedissonLock.java
@Override
public void lock(long leaseTime, TimeUnit unit) {
try {
lockInterruptibly(leaseTime, unit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit,
long threadId, RedisStrictCommand<T> command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
// 针对 redis 执行 lua脚本
return commandExecutor
.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " + // 是否存在对应的 key
"redis.call('hset', KEYS[1], ARGV[2], 1); " + // 设置 key 的值
"redis.call('pexpire', KEYS[1], ARGV[1]); " + // 设置 key 过期时间
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " + // 自增1, 加锁多少次
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);", // 返回当前 key 有效的存活期
// 分别对应: KEYS[1]、ARGV[1]、ARGV[2]
Collections.<Object>singletonList(getName()),
internalLockLeaseTime, getLockName(threadId));
}
用一张图来解释:
Redis Cluster
读写操作时,会基于key
计算slot
,得到哪台master
机子
private NodeSource getNodeSource(String key) {
// 集群模式下:CRC16.crc16(key.getBytes()) % 16384
int slot = connectionManager.calcSlot(key);
// 计算 master 实例:slot 属于哪个 master 实例
MasterSlaveEntry entry = connectionManager.getEntry(slot);
return new NodeSource(entry, slot);
}
- 执行
lua
脚本 - 启动看门狗(
watchdog
)
Redisson
配置如下:
Config config = new Config();
config.useClusterServers()
.addNodeAddress("redis://172.18.1.23:7003")
.addNodeAddress("redis://172.18.1.23:7004")
.addNodeAddress("redis://172.18.1.24:7003")
.addNodeAddress("redis://172.18.1.24:7004")
.addNodeAddress("redis://172.18.1.26:7003")
.addNodeAddress("redis://172.18.1.26:7004")
.setPassword("123456");
RedissonClient redisson = Redisson.create(config);
(2)可重入锁源码剖析之看门狗维持加锁
看门狗(
watchdog
):在后台会不断去延长key
对应的分布式锁的生存周期。
带着问题看源码:
- 分布式锁加锁成功后,
watchdog
如何延长对应key
的生存时间? - 如果长期持有一把锁(不释放),锁对应的
key
的生存时间如何变化? - 如果持有锁的机器宕机了,
watchdog
如何处理?
1)分布式锁加锁成功后,watchdog
如何延长对应 key
的生存时间?
private Long tryAcquire(long leaseTime, TimeUnit unit) {
return get(tryAcquireAsync(leaseTime, unit, Thread.currentThread().getId()));
}
private RFuture<Boolean> tryAcquireOnceAsync(long leaseTime,
TimeUnit unit, final long threadId) {
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId,
RedisCommands.EVAL_NULL_BOOLEAN);
}
// 执行 lua 脚本
RFuture<Boolean> ttlRemainingFuture =
tryLockInnerAsync(LOCK_EXPIRATION_INTERVAL_SECONDS, TimeUnit.SECONDS,
threadId, RedisCommands.EVAL_NULL_BOOLEAN);
// 添加监听器
ttlRemainingFuture.addListener(new FutureListener<Boolean>() {
@Override
public void operationComplete(Future<Boolean> future) throws Exception {
if (!future.isSuccess()) {
return;
}
// 对应 key 是否有存活时间
Boolean ttlRemaining = future.getNow();
// lock acquired
if (ttlRemaining) {
// 重点:调度过期续命
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
来详细看下 scheduleExpirationRenewal()
:
private void scheduleExpirationRenewal(final long threadId) {
if (expirationRenewalMap.containsKey(getEntryName())) {
return;
}
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
// 这个 lua 脚本:更新过期时间
RFuture<Boolean> future = commandExecutor.evalWriteAsync(
getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + "redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " + // 设置成功,表明持有锁的客户端还在
"end; " +
"return 0;", // 设置失败,表明持有锁的客户端不在
Collections.<Object>singletonList(getName()),
internalLockLeaseTime, getLockName(threadId));
// 添加监听器
future.addListener(new FutureListener<Boolean>() {
@Override
public void operationComplete(Future<Boolean> future) throws Exception {
// 执行成功,删除之前的调度
expirationRenewalMap.remove(getEntryName());
if (!future.isSuccess()) {
log.error("Can't update lock " + getName()
+ " expiration", future.cause());
return;
}
if (future.getNow()) {
// reschedule itself
// 这边递归又调用次
scheduleExpirationRenewal(threadId);
}
}
});
}
// internalLockLeaseTime 默认时间 30秒
// 所以这每 10秒 就会调用一次
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
// 取消
if (expirationRenewalMap.putIfAbsent(getEntryName(), task) != null) {
task.cancel();
}
}
来看下,分布式锁存储在 Redis
是什么:
# Redis 中如下
172.18.1.23:7004> hgetall myLock
1) "dfd3aabb-82ce-4c54-966d-5719675a3d62:1"
2) "1"
# 实际上就类似 map:
# "dfd3aabb-82ce-4c54-966d-5719675a3d62:1",代表某客户端,1 是 threadId
# 1,代表自增次数
{
"dfd3aabb-82ce-4c54-966d-5719675a3d62:1": 1
}
可以看到执行调度任务的默认时间是:10秒,internalLockLeaseTime / 3 = 10
,默认 internalLockLeaseTime = 30秒
。
2)如果长期持有一把锁(不释放),锁对应的 key
的生存时间如何变化?
先在代码处打个断点:
再用 redis-cli
看锁对应的变化:
# 是否存在对应的 key
exists myLock
# 获取 myLock key的值
hgetall myLock
# 获取 myLock key的生存时间
pttl myLock
发现结果跟源码执行的逻辑一致:
- 分布式锁对应的
key
的生存时间不断减少 - 每次减少了 10秒,到剩余 20秒生存时间的时候,又被刷新为 30秒
3)如果持有锁的机器宕机了,watchdog
如何处理?
机子都宕机了,看门狗当然也没有。
对应的分布式锁就会在 30秒后过期。
其他客户端就可以在 30秒后获取到这把锁。