Skip to content

Commit 1d35053

Browse files
committed
[docs update]Redisson 看门狗原理
1 parent e29154b commit 1d35053

File tree

2 files changed

+84
-8
lines changed

2 files changed

+84
-8
lines changed

docs/distributed-system/distributed-lock.md

Lines changed: 84 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
---
22
title: 分布式锁
33
category: 分布式
4-
5-
64
---
75

86
网上有很多分布式锁相关的文章,写了一个相对简洁易懂的版本,针对面试和工作应该够用了。
@@ -19,7 +17,7 @@ category: 分布式
1917

2018
分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。于是,**分布式锁** 就诞生了。
2119

22-
举个例子:系统的订单服务一共部署了 3 份,都对外提供服务。用户下订单之前需要检查库存,为了防止超卖,这里需要加锁以实现对检查库存操作的同步访问。由于订单服务位于不同的 JVM 进程中,本地锁在这种情况下就没办法正常工作了。我们需要用到分布式锁,这样的话,即使多个线程不在同一个 JVM 进程中也能获取到同一把锁,进而实现共享资源的互斥访问。
20+
举个例子:系统的订单服务一共部署了 3 份,都对外提供服务。用户下订单之前需要检查库存,为了防止超卖,这里需要加锁以实现对检查库存操作的同步访问。由于订单服务位于不同的 JVM 进程中,本地锁在这种情况下就没办法正常工作了。我们需要用到分布式锁,这样的话,即使多个线程不在同一个 JVM 进程中也能获取到同一把锁,进而实现共享资源的互斥访问。
2321

2422
下面是我对分布式锁画的一张示意图。
2523

@@ -69,6 +67,8 @@ else
6967
end
7068
```
7169

70+
![Redis 实现简易分布式锁](images/distributed-lock/distributed-lock-setnx.png)
71+
7272
这是一种最简易的 Redis 分布式锁实现,实现方式比较简单,性能也很高效。不过,这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。
7373

7474
### 为什么要给锁设置一个过期时间?
@@ -91,28 +91,105 @@ OK
9191

9292
你或许在想: **如果操作共享资源的操作还未完成,锁过期时间能够自己续期就好了!**
9393

94+
### 如何实现锁的优雅续期?
95+
9496
对于 Java 开发的小伙伴来说,已经有了现成的解决方案:**[Redisson](https://github.com/redisson/redisson)** 。其他语言的解决方案,可以在 Redis 官方文档中找到,地址:https://redis.io/topics/distlock
9597

9698
![Distributed locks with Redis](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/github/javaguide/redis-distributed-lock.png)
9799

98-
Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。
100+
Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。并且,Redisson 还支持 Redis 单机、Redis Sentinel 、Redis Cluster 等多种部署架构。
101+
102+
Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 **Watch Dog( 看门狗)**,如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。
103+
104+
看门狗名字的由来于 `getLockWatchdogTimeou()` 方法,这个方法返回的是看门狗给锁续期的过期时间,默认为 30 秒([redisson-3.17.6](https://github.com/redisson/redisson/releases/tag/redisson-3.17.6))。
105+
106+
```java
107+
//默认 30秒,支持修改
108+
private long lockWatchdogTimeout = 30 * 1000;
109+
110+
public Config setLockWatchdogTimeout(long lockWatchdogTimeout) {
111+
this.lockWatchdogTimeout = lockWatchdogTimeout;
112+
return this;
113+
}
114+
public long getLockWatchdogTimeout() {
115+
return lockWatchdogTimeout;
116+
}
117+
```
118+
119+
`renewExpiration()` 方法包含了看门狗的主要逻辑:
120+
121+
```java
122+
private void renewExpiration() {
123+
//......
124+
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
125+
@Override
126+
public void run(Timeout timeout) throws Exception {
127+
//......
128+
// 异步续期,基于 Lua 脚本
129+
CompletionStage<Boolean> future = renewExpirationAsync(threadId);
130+
future.whenComplete((res, e) -> {
131+
if (e != null) {
132+
// 无法续期
133+
log.error("Can't update lock " + getRawName() + " expiration", e);
134+
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
135+
return;
136+
}
137+
138+
if (res) {
139+
// 递归调用实现续期
140+
renewExpiration();
141+
} else {
142+
// 取消续期
143+
cancelExpirationRenewal(null);
144+
}
145+
});
146+
}
147+
// 延迟 internalLockLeaseTime/3(默认 10s,也就是 30/3) 再调用
148+
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
149+
150+
ee.setTimeout(task);
151+
}
152+
```
153+
154+
默认情况下,每过 10 秒,看门狗就会执行续期操作,将锁的超时时间设置为 30 秒。看门狗续期前也会先判断是否需要执行续期操作,需要才会执行续期,否则取消续期操作。
99155

100-
Redisson 中的分布式锁自带自动续期机制,它提供了一个专门用来监控锁的 **Watch Dog( 看门狗)**,如果操作共享资源的还未完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。
156+
Watch Dog 通过调用 `renewExpirationAsync()` 方法实现锁的异步续期:
157+
158+
```java
159+
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
160+
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
161+
// 如果指定的锁存在就续期,将其过期时间设置为 30s(默认)
162+
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
163+
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
164+
"return 1; " +
165+
"end; " +
166+
"return 0;",
167+
Collections.singletonList(getRawName()),
168+
internalLockLeaseTime, getLockName(threadId));
169+
}
170+
```
171+
172+
可以看出, `renewExpirationAsync` 方法其实是调用 Lua 脚本实现的续期,这样做主要是为了保证续期操作的原子性。
101173

102174
我这里以 Redisson 的分布式可重入锁 `RLock` 为例来说明如何使用 Redisson 实现分布式锁:
103175

104176
```java
105177
// 1.获取指定的分布式锁对象
106178
RLock lock = redisson.getLock("lock");
107-
// 2.拿锁,具有 Watch Dog 自动续期机制
179+
// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制
108180
lock.lock();
109181
// 3.执行业务
110182
...
111183
// 4.释放锁
112184
lock.unlock();
113185
```
114186

115-
可以看出,代码非常简洁直观。
187+
只有未指定锁超时时间,才会使用到 Watch Dog 自动续期机制。
188+
189+
```java
190+
// 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制
191+
lock.lock(10, TimeUnit.SECONDS);
192+
```
116193

117194
如果使用 Redis 来实现分布式锁的话,还是比较推荐直接基于 Redisson 来做的。
118195

@@ -135,4 +212,3 @@ Redlock 实现比较复杂,性能比较差,发生时钟变迁的情况下还
135212
实际项目中不建议使用 Redlock 算法,成本和收益不成正比。
136213

137214
如果不是非要实现绝对可靠的分布式锁的话,其实单机版 Redis 就完全够了,实现简单,性能也非常高。如果你必须要实现一个绝对可靠的分布式锁的话,可以基于 Zookeeper 来做,只是性能会差一些。
138-
21.4 KB
Loading

0 commit comments

Comments
 (0)