1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-06-16 18:10:13 +08:00

[docs update]完善分布式锁的介绍

This commit is contained in:
Guide 2023-06-13 21:47:00 +08:00
parent d904068e98
commit 781fd22f77
15 changed files with 440 additions and 375 deletions

View File

@ -358,7 +358,8 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8 ](https://docs.oracle
### 分布式锁
[分布式锁常见知识点&面试题总结](https://javaguide.cn/distributed-system/distributed-lock.html)
- [分布式锁介绍](https://javaguide.cn/distributed-system/distributed-lock.html)
- [分布式锁常见实现方案总结](https://javaguide.cn/distributed-system/distributed-lock-implementations.html)
### 分布式事务

View File

@ -2,11 +2,6 @@ import { navbar } from "vuepress-theme-hope";
export default navbar([
{ text: "面试指南", icon: "java", link: "/home.md" },
{
text: "知识星球",
icon: "planet",
link: "/about-the-author/zhishixingqiu-two-years.md",
},
{ text: "开源项目", icon: "github", link: "/open-source-project/" },
{ text: "技术书籍", icon: "book", link: "/books/" },
{
@ -14,6 +9,22 @@ export default navbar([
icon: "article",
link: "/high-quality-technical-articles/",
},
{
text: "知识星球",
icon: "planet",
children: [
{
text: "星球介绍",
icon: "about",
link: "/about-the-author/zhishixingqiu-two-years.md",
},
{
text: "星球优质主题汇总",
icon: "star",
link: "https://www.yuque.com/snailclimb/rpkqw1/ncxpnfmlng08wlf1",
},
],
},
{
text: "网站相关",
icon: "about",

View File

@ -472,7 +472,7 @@ export default sidebar({
{
text: "分布式锁",
icon: "lock",
children: ["distributed-lock"],
children: ["distributed-lock", "distributed-lock-implementations"],
},
{
text: "RPC",

View File

@ -0,0 +1,366 @@
---
title: 分布式锁常见实现方案总结
category: 分布式
---
通常情况下,我们一般会选择基于 Redis 或者 ZooKeeper 实现分布式锁Redis 用的要更多一点,我这里也先以 Redis 为例介绍分布式锁的实现。
## 基于 Redis 实现分布式锁
### 如何基于 Redis 实现一个最简易的分布式锁?
不论是本地锁还是分布式锁,核心都在于“互斥”。
在 Redis 中, `SETNX` 命令是可以帮助我们实现互斥。`SETNX`**SET** if **N**ot e**X**ists (对应 Java 中的 `setIfAbsent` 方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, `SETNX` 啥也不做。
```bash
> SETNX lockKey uniqueValue
(integer) 1
> SETNX lockKey uniqueValue
(integer) 0
```
释放锁的话,直接通过 `DEL` 命令删除对应的 key 即可。
```bash
> DEL lockKey
(integer) 1
```
为了防止误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value唯一值来判断。
选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。
```lua
// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
```
![Redis 实现简易分布式锁](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-setnx.png)
这是一种最简易的 Redis 分布式锁实现,实现方式比较简单,性能也很高效。不过,这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。
### 为什么要给锁设置一个过期时间?
为了避免锁无法被释放,我们可以想到的一个解决办法就是:**给这个 key也就是锁 设置一个过期时间** 。
```bash
127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX
OK
```
- **lockKey**:加锁的锁名;
- **uniqueValue**:能够唯一标示锁的随机字符串;
- **NX**:只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功;
- **EX**过期时间设置秒为单位EX 3 标示这个锁有一个 3 秒的自动过期时间。与 EX 对应的是 PX毫秒为单位这两个都是过期时间设置。
**一定要保证设置指定 key 的值和过期时间是一个原子操作!!!** 不然的话,依然可能会出现锁无法被释放的问题。
这样确实可以解决问题,不过,这种解决办法同样存在漏洞:**如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。如果锁的超时时间设置过长,又会影响到性能。**
你或许在想:**如果操作共享资源的操作还未完成,锁过期时间能够自己续期就好了!**
### 如何实现锁的优雅续期?
对于 Java 开发的小伙伴来说,已经有了现成的解决方案:**[Redisson](https://github.com/redisson/redisson)** 。其他语言的解决方案,可以在 Redis 官方文档中找到,地址:<https://redis.io/topics/distlock>
![Distributed locks with Redis](https://oss.javaguide.cn/github/javaguide/redis-distributed-lock.png)
Redisson 是一个开源的 Java 语言 Redis 客户端提供了很多开箱即用的功能不仅仅包括多种分布式锁的实现。并且Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。
Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 **Watch Dog 看门狗)**如果操作共享资源的线程还未执行完成的话Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。
![Redisson 看门狗自动续期](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-redisson-renew-expiration.png)
看门狗名字的由来于 `getLockWatchdogTimeout()` 方法,这个方法返回的是看门狗给锁续期的过期时间,默认为 30 秒([redisson-3.17.6](https://github.com/redisson/redisson/releases/tag/redisson-3.17.6))。
```java
//默认 30秒支持修改
private long lockWatchdogTimeout = 30 * 1000;
public Config setLockWatchdogTimeout(long lockWatchdogTimeout) {
this.lockWatchdogTimeout = lockWatchdogTimeout;
return this;
}
public long getLockWatchdogTimeout() {
return lockWatchdogTimeout;
}
```
`renewExpiration()` 方法包含了看门狗的主要逻辑:
```java
private void renewExpiration() {
//......
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
//......
// 异步续期,基于 Lua 脚本
CompletionStage<Boolean> future = renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
if (e != null) {
// 无法续期
log.error("Can't update lock " + getRawName() + " expiration", e);
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
if (res) {
// 递归调用实现续期
renewExpiration();
} else {
// 取消续期
cancelExpirationRenewal(null);
}
});
}
// 延迟 internalLockLeaseTime/3默认 10s也就是 30/3 再调用
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
```
默认情况下,每过 10 秒,看门狗就会执行续期操作,将锁的超时时间设置为 30 秒。看门狗续期前也会先判断是否需要执行续期操作,需要才会执行续期,否则取消续期操作。
Watch Dog 通过调用 `renewExpirationAsync()` 方法实现锁的异步续期:
```java
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 判断是否为持锁线程,如果是就执行续期操作,就锁的过期时间设置为 30s默认
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getRawName()),
internalLockLeaseTime, getLockName(threadId));
}
```
可以看出, `renewExpirationAsync` 方法其实是调用 Lua 脚本实现的续期,这样做主要是为了保证续期操作的原子性。
我这里以 Redisson 的分布式可重入锁 `RLock` 为例来说明如何使用 Redisson 实现分布式锁:
```java
// 1.获取指定的分布式锁对象
RLock lock = redisson.getLock("lock");
// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制
lock.lock();
// 3.执行业务
...
// 4.释放锁
lock.unlock();
```
只有未指定锁超时时间,才会使用到 Watch Dog 自动续期机制。
```java
// 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制
lock.lock(10, TimeUnit.SECONDS);
```
如果使用 Redis 来实现分布式锁的话,还是比较推荐直接基于 Redisson 来做的。
### 如何实现可重入锁?
所谓可重入锁指的是在一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法即可重入 ,而无需重新获得锁。像 Java 中的 `synchronized``ReentrantLock` 都属于可重入锁。
**不可重入的分布式锁基本可以满足绝大部分业务场景了,一些特殊的场景可能会需要使用可重入的分布式锁。**
可重入分布式锁的实现核心思路是线程在获取锁的时候判断是否为自己的锁,如果是的话,就不用再重新获取了。为此,我们可以为每个锁关联一个可重入计数器和一个占有它的线程。当可重入计数器大于 0 时,则锁被占有,需要判断占有该锁的线程和请求获取锁的线程是否为同一个。
实际项目中,我们不需要自己手动实现,推荐使用我们上面提到的 **Redisson** 其内置了多种类型的锁比如可重入锁Reentrant Lock、自旋锁Spin Lock、公平锁Fair Lock、多重锁MultiLock、 红锁RedLock、 读写锁ReadWriteLock
![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/redisson-readme-locks.png)
### Redis 如何解决集群情况下分布式锁的可靠性?
为了避免单点故障,生产环境下的 Redis 服务通常是集群化部署的。
Redis 集群下,上面介绍到的分布式锁的实现会存在一些问题。由于 Redis 集群数据同步到各个节点时是异步的,如果在 Redis 主节点获取到锁后在没有同步到其他节点时Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。
![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/redis-master-slave-distributed-lock.png)
针对这个问题Redis 之父 antirez 设计了 [Redlock 算法](https://redis.io/topics/distlock) 来解决。
![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-redis.io-realock.png)
Redlock 算法的思想是让客户端向 Redis 集群中的多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
即使部分 Redis 节点出现问题,只要保证 Redis 集群中有半数以上的 Redis 节点可用,分布式锁服务就是正常的。
Redlock 是直接操作 Redis 节点的,并不是通过 Redis 集群操作的,这样才可以避免 Redis 集群主从切换导致的锁丢失问题。
Redlock 实现比较复杂,性能比较差,发生时钟变迁的情况下还存在安全性隐患。《数据密集型应用系统设计》一书的作者 Martin Kleppmann 曾经专门发文([How to do distributed locking - Martin Kleppmann - 2016](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html))怼过 Redlock他认为这是一个很差的分布式锁实现。感兴趣的朋友可以看看[Redis 锁从面试连环炮聊到神仙打架](https://mp.weixin.qq.com/s?__biz=Mzg3NjU3NTkwMQ==&mid=2247505097&idx=1&sn=5c03cb769c4458350f4d4a321ad51f5a&source=41#wechat_redirect)这篇文章,有详细介绍到 antirez 和 Martin Kleppmann 关于 Redlock 的激烈辩论。
实际项目中不建议使用 Redlock 算法,成本和收益不成正比。
如果不是非要实现绝对可靠的分布式锁的话,其实单机版 Redis 就完全够了,实现简单,性能也非常高。如果你必须要实现一个绝对可靠的分布式锁的话,可以基于 ZooKeeper 来做,只是性能会差一些。
## 基于 ZooKeeper 实现分布式锁
Redis 实现分布式锁性能较高ZooKeeper 实现分布式锁可靠性更高。实际项目中,我们应该根据业务的具体需求来选择。
### 如何基于 ZooKeeper 实现分布式锁?
ZooKeeper 分布式锁是基于 **临时顺序节点****Watcher事件监听器** 实现的。
获取锁:
1. 首先我们要有一个持久节点`/locks`,客户端获取锁就是在`locks`下创建临时顺序节点。
2. 假设客户端 1 创建了`/locks/lock1`节点,创建成功之后,会判断 `lock1`是否是 `/locks` 下最小的子节点。
3. 如果 `lock1`是最小的子节点,则获取锁成功。否则,获取锁失败。
4. 如果获取锁失败,则说明有其他的客户端已经成功获取锁。客户端 1 并不会不停地循环去尝试加锁,而是在前一个节点比如`/locks/lock0`上注册一个事件监听器。这个监听器的作用是当前一个节点释放锁之后通知客户端 1避免无效自旋这样客户端 1 就加锁成功了。
释放锁:
1. 成功获取锁的客户端在执行完业务流程之后,会将对应的子节点删除。
2. 成功获取锁的客户端在出现故障之后,对应的子节点由于是临时顺序节点,也会被自动删除,避免了锁无法被释放。
3. 我们前面说的事件监听器其实监听的就是这个子节点删除事件,子节点删除就意味着锁被释放。
![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-zookeeper.png)
实际项目中,推荐使用 Curator 来实现 ZooKeeper 分布式锁。Curator 是 Netflix 公司开源的一套 ZooKeeper Java 客户端框架,相比于 ZooKeeper 自带的客户端 zookeeper 来说Curator 的封装更加完善,各种 API 都可以比较方便地使用。
`Curator`主要实现了下面四种锁:
- `InterProcessMutex`:分布式可重入排它锁
- `InterProcessSemaphoreMutex`:分布式不可重入排它锁
- `InterProcessReadWriteLock`:分布式读写锁
- `InterProcessMultiLock`:将多个锁作为单个实体管理的容器,获取锁的时候获取所有锁,释放锁也会释放所有锁资源(忽略释放失败的锁)。
```java
CuratorFramework client = ZKUtils.getClient();
client.start();
// 分布式可重入排它锁
InterProcessLock lock1 = new InterProcessMutex(client, lockPath1);
// 分布式不可重入排它锁
InterProcessLock lock2 = new InterProcessSemaphoreMutex(client, lockPath2);
// 将多个锁作为一个整体
InterProcessMultiLock lock = new InterProcessMultiLock(Arrays.asList(lock1, lock2));
if (!lock.acquire(10, TimeUnit.SECONDS)) {
throw new IllegalStateException("不能获取多锁");
}
System.out.println("已获取多锁");
System.out.println("是否有第一个锁: " + lock1.isAcquiredInThisProcess());
System.out.println("是否有第二个锁: " + lock2.isAcquiredInThisProcess());
try {
// 资源操作
resource.use();
} finally {
System.out.println("释放多个锁");
lock.release();
}
System.out.println("是否有第一个锁: " + lock1.isAcquiredInThisProcess());
System.out.println("是否有第二个锁: " + lock2.isAcquiredInThisProcess());
client.close();
```
### 为什么要用临时顺序节点?
每个数据节点在 ZooKeeper 中被称为 **znode**,它是 ZooKeeper 中数据的最小单元。
我们通常是将 znode 分为 4 大类:
- **持久PERSISTENT节点**:一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。
- **临时EPHEMERAL节点**:临时节点的生命周期是与 **客户端会话session** 绑定的,**会话消失则节点消失** 。并且,**临时节点只能做叶子节点** ,不能创建子节点。
- **持久顺序PERSISTENT_SEQUENTIAL节点**除了具有持久PERSISTENT节点的特性之外 子节点的名称还具有顺序性。比如 `/node1/app0000000001``/node1/app0000000002`
- **临时顺序EPHEMERAL_SEQUENTIAL节点**除了具备临时EPHEMERAL节点的特性之外子节点的名称还具有顺序性。
可以看出,临时节点相比持久节点,最主要的是对会话失效的情况处理不一样,临时节点会话消失则对应的节点消失。这样的话,如果客户端发生异常导致没来得及释放锁也没关系,会话失效节点自动被删除,不会发生死锁的问题。
使用 Redis 实现分布式锁的时候,我们是通过过期时间来避免锁无法被释放导致死锁问题的,而 ZooKeeper 直接利用临时节点的特性即可。
假设不适用顺序节点的话,所有尝试获取锁的客户端都会对持有锁的子节点加监听器。当该锁被释放之后,势必会造成所有尝试获取锁的客户端来争夺锁,这样对性能不友好。使用顺序节点之后,只需要监听前一个节点就好了,对性能更友好。
### 为什么要设置对前一个节点的监听?
> Watcher事件监听器是 ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher并且在一些特定事件触发的时候ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。
同一时间段内,可能会有很多客户端同时获取锁,但只有一个可以获取成功。如果获取锁失败,则说明有其他的客户端已经成功获取锁。获取锁失败的客户端并不会不停地循环去尝试加锁,而是在前一个节点注册一个事件监听器。
这个事件监听器的作用是:**当前一个节点对应的客户端释放锁之后也就是前一个节点被删除之后监听的是删除事件通知获取锁失败的客户端唤醒等待的线程Java 中的 `wait/notifyAll` ),让它尝试去获取锁,然后就成功获取锁了。**
### 如何实现可重入锁?
这里以 Curator 的 `InterProcessMutex` 对可重入锁的实现来介绍(源码地址:[InterProcessMutex.java](https://github.com/apache/curator/blob/master/curator-recipes/src/main/java/org/apache/curator/framework/recipes/locks/InterProcessMutex.java))。
当我们调用 `InterProcessMutex#acquire`方法获取锁的时候,会调用`InterProcessMutex#internalLock`方法。
```java
// 获取可重入互斥锁,直到获取成功为止
@Override
public void acquire() throws Exception {
if (!internalLock(-1, null)) {
throw new IOException("Lost connection while trying to acquire lock: " + basePath);
}
}
```
`internalLock` 方法会先获取当前请求锁的线程,然后从 `threadData`( `ConcurrentMap<Thread, LockData>` 类型)中获取当前线程对应的 `lockData``lockData` 包含锁的信息和加锁的次数,是实现可重入锁的关键。
第一次获取锁的时候,`lockData``null`。获取锁成功之后,会将当前线程和对应的 `lockData` 放到 `threadData`
```java
private boolean internalLock(long time, TimeUnit unit) throws Exception {
// 获取当前请求锁的线程
Thread currentThread = Thread.currentThread();
// 拿对应的 lockData
LockData lockData = threadData.get(currentThread);
// 第一次获取锁的话lockData 为 null
if (lockData != null) {
// 当前线程获取过一次锁之后
// 因为当前线程的锁存在, lockCount 自增后返回,实现锁重入.
lockData.lockCount.incrementAndGet();
return true;
}
// 尝试获取锁
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
if (lockPath != null) {
LockData newLockData = new LockData(currentThread, lockPath);
// 获取锁成功之后,将当前线程和对应的 lockData 放到 threadData 中
threadData.put(currentThread, newLockData);
return true;
}
return false;
}
```
`LockData``InterProcessMutex`中的一个静态内部类。
```java
private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();
private static class LockData
{
// 当前持有锁的线程
final Thread owningThread;
// 锁对应的子节点
final String lockPath;
// 加锁的次数
final AtomicInteger lockCount = new AtomicInteger(1);
private LockData(Thread owningThread, String lockPath)
{
this.owningThread = owningThread;
this.lockPath = lockPath;
}
}
```
如果已经获取过一次锁,后面再来获取锁的话,直接就会在 `if (lockData != null)` 这里被拦下了,然后就会执行`lockData.lockCount.incrementAndGet();` 将加锁次数加 1。
整个可重入锁的实现逻辑非常简单,直接在客户端判断当前线程有没有获取锁,有的话直接将加锁次数加 1 就可以了。
## 总结
这篇文章我们介绍了实现分布式锁的两种常见方式。至于具体选择 Redis 还是 ZooKeeper 来实现分布式锁,还是要看业务的具体需求。如果对性能要求比较高的话,建议使用 Redis 实现分布式锁。如果对可靠性要求比较高的话,建议使用 ZooKeeper 实现分布式锁。

View File

@ -1,11 +1,32 @@
---
title: 分布式锁常见问题总结
title: 分布式锁介绍
category: 分布式
---
网上有很多分布式锁相关的文章,写了一个相对简洁易懂的版本,针对面试和工作应该够用了。
## 分布式锁介绍
这篇文章我们先介绍一下分布式锁的基本概念。
## 为什么需要分布式锁?
在多线程环境中,如果多个线程同时访问共享资源(例如商品库存、外卖订单),会发生数据竞争,可能会导致出现脏数据或者系统问题,威胁到程序的正常运行。
举个例子,假设现在有 100 个用户参与某个限时秒杀活动,每位用户限购 1 件商品,且商品的数量只有 3 个。如果不对共享资源进行互斥访问,就可能出现以下情况:
- 线程 1、2、3 等多个线程同时进入抢购方法,每一个线程对应一个用户。
- 线程 1 查询用户已经抢购的数量,发现当前用户尚未抢购且商品库存还有 1 个,因此认为可以继续执行抢购流程。
- 线程 2 也执行查询用户已经抢购的数量,发现当前用户尚未抢购且商品库存还有 1 个,因此认为可以继续执行抢购流程。
- 线程 1 继续执行,将库存数量减少 1 个,然后返回成功。
- 线程 2 继续执行,将库存数量减少 1 个,然后返回成功。
- 此时就发生了超卖问题,导致商品被多卖了一份。
![共享资源未互斥访问导致出现问题](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/oversold-without-locking.png)
为了保证共享资源被安全地访问,我们需要使用互斥操作对共享资源进行保护,即同一时刻只允许一个线程访问共享资源,其他线程需要等待当前线程释放后才能访问。这样可以避免数据竞争和脏数据问题,保证程序的正确性和稳定性。
**如何才能实现共享资源的互斥访问呢?** 锁是一个比较通用的解决方案,更准确点来说是悲观锁。
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。
对于单机多线程来说,在 Java 中,我们通常使用 `ReetrantLock` 类、`synchronized` 关键字这类 JDK 自带的 **本地锁** 来控制一个 JVM 进程内的多个线程对本地共享资源的访问。
@ -25,371 +46,35 @@ category: 分布式
从图中可以看出,这些独立的进程中的线程访问共享资源是互斥的,同一时刻只有一个线程可以获取到分布式锁访问共享资源。
## 分布式锁应该具备哪些条件?
一个最基本的分布式锁需要满足:
- **互斥**:任意一个时刻,锁只能被一个线程持有
- **高可用**:锁服务是高可用的。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。
- **互斥**:任意一个时刻,锁只能被一个线程持有
- **高可用**:锁服务是高可用的,当一个锁服务出现问题,能够自动切换到另外一个锁服务。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。这一般是通过超时机制实现的。
- **可重入**:一个节点获取了锁之后,还可以再次获取锁。
通常情况下,我们一般会选择基于 Redis 或者 ZooKeeper 实现分布式锁Redis 用的要更多一点,我这里也以 Redis 为例介绍分布式锁的实现。
除了上面这三个基本条件之外,一个好的分布式锁还需要满足下面这些条件:
## 基于 Redis 实现分布式锁
- **高性能**:获取和释放锁的操作应该快速完成,并且不应该对整个系统的性能造成过大影响。
- **非阻塞**:如果获取不到锁,不能无限期等待,避免对系统正常运行造成影响。
### 如何基于 Redis 实现一个最简易的分布式锁?
## 分布式锁的常见实现方式有哪些
不论是本地锁还是分布式锁,核心都在于“互斥”。
常见分布式锁实现方案如下:
在 Redis 中, `SETNX` 命令是可以帮助我们实现互斥。`SETNX`**SET** if **N**ot e**X**ists (对应 Java 中的 `setIfAbsent` 方法),如果 key 不存在的话,才会设置 key 的值。如果 key 已经存在, `SETNX` 啥也不做。
- 基于关系型数据库比如 MySQL 实现分布式锁。
- 基于分布式协调服务 ZooKeeper 实现分布式锁。
- 基于分布式键值存储系统比如 Redis 、Etcd 实现分布式锁。
```bash
> SETNX lockKey uniqueValue
(integer) 1
> SETNX lockKey uniqueValue
(integer) 0
```
关系型数据库的方式一般是通过唯一索引或者排他锁实现。不过,一般不会使用这种方式,问题太多比如性能太差、不具备锁失效机制。
释放锁的话,直接通过 `DEL` 命令删除对应的 key 即可。
```bash
> DEL lockKey
(integer) 1
```
为了防止误删到其他的锁,这里我们建议使用 Lua 脚本通过 key 对应的 value唯一值来判断。
选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。
```lua
// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
```
![Redis 实现简易分布式锁](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-setnx.png)
这是一种最简易的 Redis 分布式锁实现,实现方式比较简单,性能也很高效。不过,这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。
### 为什么要给锁设置一个过期时间?
为了避免锁无法被释放,我们可以想到的一个解决办法就是:**给这个 key也就是锁 设置一个过期时间** 。
```bash
127.0.0.1:6379> SET lockKey uniqueValue EX 3 NX
OK
```
- **lockKey**:加锁的锁名;
- **uniqueValue**:能够唯一标示锁的随机字符串;
- **NX**:只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功;
- **EX**过期时间设置秒为单位EX 3 标示这个锁有一个 3 秒的自动过期时间。与 EX 对应的是 PX毫秒为单位这两个都是过期时间设置。
**一定要保证设置指定 key 的值和过期时间是一个原子操作!!!** 不然的话,依然可能会出现锁无法被释放的问题。
这样确实可以解决问题,不过,这种解决办法同样存在漏洞:**如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。如果锁的超时时间设置过长,又会影响到性能。**
你或许在想:**如果操作共享资源的操作还未完成,锁过期时间能够自己续期就好了!**
### 如何实现锁的优雅续期?
对于 Java 开发的小伙伴来说,已经有了现成的解决方案:**[Redisson](https://github.com/redisson/redisson)** 。其他语言的解决方案,可以在 Redis 官方文档中找到,地址:<https://redis.io/topics/distlock>
![Distributed locks with Redis](https://oss.javaguide.cn/github/javaguide/redis-distributed-lock.png)
Redisson 是一个开源的 Java 语言 Redis 客户端提供了很多开箱即用的功能不仅仅包括多种分布式锁的实现。并且Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。
Redisson 中的分布式锁自带自动续期机制,使用起来非常简单,原理也比较简单,其提供了一个专门用来监控和续期锁的 **Watch Dog 看门狗)**如果操作共享资源的线程还未执行完成的话Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。
![Redisson 看门狗自动续期](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-redisson-renew-expiration.png)
看门狗名字的由来于 `getLockWatchdogTimeout()` 方法,这个方法返回的是看门狗给锁续期的过期时间,默认为 30 秒([redisson-3.17.6](https://github.com/redisson/redisson/releases/tag/redisson-3.17.6))。
```java
//默认 30秒支持修改
private long lockWatchdogTimeout = 30 * 1000;
public Config setLockWatchdogTimeout(long lockWatchdogTimeout) {
this.lockWatchdogTimeout = lockWatchdogTimeout;
return this;
}
public long getLockWatchdogTimeout() {
return lockWatchdogTimeout;
}
```
`renewExpiration()` 方法包含了看门狗的主要逻辑:
```java
private void renewExpiration() {
//......
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
//......
// 异步续期,基于 Lua 脚本
CompletionStage<Boolean> future = renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
if (e != null) {
// 无法续期
log.error("Can't update lock " + getRawName() + " expiration", e);
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
if (res) {
// 递归调用实现续期
renewExpiration();
} else {
// 取消续期
cancelExpirationRenewal(null);
}
});
}
// 延迟 internalLockLeaseTime/3默认 10s也就是 30/3 再调用
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
```
默认情况下,每过 10 秒,看门狗就会执行续期操作,将锁的超时时间设置为 30 秒。看门狗续期前也会先判断是否需要执行续期操作,需要才会执行续期,否则取消续期操作。
Watch Dog 通过调用 `renewExpirationAsync()` 方法实现锁的异步续期:
```java
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
// 判断是否为持锁线程,如果是就执行续期操作,就锁的过期时间设置为 30s默认
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getRawName()),
internalLockLeaseTime, getLockName(threadId));
}
```
可以看出, `renewExpirationAsync` 方法其实是调用 Lua 脚本实现的续期,这样做主要是为了保证续期操作的原子性。
我这里以 Redisson 的分布式可重入锁 `RLock` 为例来说明如何使用 Redisson 实现分布式锁:
```java
// 1.获取指定的分布式锁对象
RLock lock = redisson.getLock("lock");
// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制
lock.lock();
// 3.执行业务
...
// 4.释放锁
lock.unlock();
```
只有未指定锁超时时间,才会使用到 Watch Dog 自动续期机制。
```java
// 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制
lock.lock(10, TimeUnit.SECONDS);
```
如果使用 Redis 来实现分布式锁的话,还是比较推荐直接基于 Redisson 来做的。
### 如何实现可重入锁?
所谓可重入锁指的是在一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法即可重入 ,而无需重新获得锁。像 Java 中的 `synchronized``ReentrantLock` 都属于可重入锁。
**不可重入的分布式锁基本可以满足绝大部分业务场景了,一些特殊的场景可能会需要使用可重入的分布式锁。**
可重入分布式锁的实现核心思路是线程在获取锁的时候判断是否为自己的锁,如果是的话,就不用再重新获取了。为此,我们可以为每个锁关联一个可重入计数器和一个占有它的线程。当可重入计数器大于 0 时,则锁被占有,需要判断占有该锁的线程和请求获取锁的线程是否为同一个。
实际项目中,我们不需要自己手动实现,推荐使用我们上面提到的 **Redisson** 其内置了多种类型的锁比如可重入锁Reentrant Lock、自旋锁Spin Lock、公平锁Fair Lock、多重锁MultiLock、 红锁RedLock、 读写锁ReadWriteLock
![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/redisson-readme-locks.png)
### Redis 如何解决集群情况下分布式锁的可靠性?
为了避免单点故障,生产环境下的 Redis 服务通常是集群化部署的。
Redis 集群下,上面介绍到的分布式锁的实现会存在一些问题。由于 Redis 集群数据同步到各个节点时是异步的,如果在 Redis 主节点获取到锁后在没有同步到其他节点时Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。
![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/redis-master-slave-distributed-lock.png)
针对这个问题Redis 之父 antirez 设计了 [Redlock 算法](https://redis.io/topics/distlock) 来解决。
![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-redis.io-realock.png)
Redlock 算法的思想是让客户端向 Redis 集群中的多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
即使部分 Redis 节点出现问题,只要保证 Redis 集群中有半数以上的 Redis 节点可用,分布式锁服务就是正常的。
Redlock 是直接操作 Redis 节点的,并不是通过 Redis 集群操作的,这样才可以避免 Redis 集群主从切换导致的锁丢失问题。
Redlock 实现比较复杂,性能比较差,发生时钟变迁的情况下还存在安全性隐患。《数据密集型应用系统设计》一书的作者 Martin Kleppmann 曾经专门发文([How to do distributed locking - Martin Kleppmann - 2016](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html))怼过 Redlock他认为这是一个很差的分布式锁实现。感兴趣的朋友可以看看[Redis 锁从面试连环炮聊到神仙打架](https://mp.weixin.qq.com/s?__biz=Mzg3NjU3NTkwMQ==&mid=2247505097&idx=1&sn=5c03cb769c4458350f4d4a321ad51f5a&source=41#wechat_redirect)这篇文章,有详细介绍到 antirez 和 Martin Kleppmann 关于 Redlock 的激烈辩论。
实际项目中不建议使用 Redlock 算法,成本和收益不成正比。
如果不是非要实现绝对可靠的分布式锁的话,其实单机版 Redis 就完全够了,实现简单,性能也非常高。如果你必须要实现一个绝对可靠的分布式锁的话,可以基于 ZooKeeper 来做,只是性能会差一些。
## 基于 ZooKeeper 实现分布式锁
Redis 实现分布式锁性能较高ZooKeeper 实现分布式锁可靠性更高。实际项目中,我们应该根据业务的具体需求来选择。
### 如何基于 ZooKeeper 实现分布式锁?
ZooKeeper 分布式锁是基于 **临时顺序节点****Watcher事件监听器** 实现的。
获取锁:
1. 首先我们要有一个持久节点`/locks`,客户端获取锁就是在`locks`下创建临时顺序节点。
2. 假设客户端 1 创建了`/locks/lock1`节点,创建成功之后,会判断 `lock1`是否是 `/locks` 下最小的子节点。
3. 如果 `lock1`是最小的子节点,则获取锁成功。否则,获取锁失败。
4. 如果获取锁失败,则说明有其他的客户端已经成功获取锁。客户端 1 并不会不停地循环去尝试加锁,而是在前一个节点比如`/locks/lock0`上注册一个事件监听器。这个监听器的作用是当前一个节点释放锁之后通知客户端 1避免无效自旋这样客户端 1 就加锁成功了。
释放锁:
1. 成功获取锁的客户端在执行完业务流程之后,会将对应的子节点删除。
2. 成功获取锁的客户端在出现故障之后,对应的子节点由于是临时顺序节点,也会被自动删除,避免了锁无法被释放。
3. 我们前面说的事件监听器其实监听的就是这个子节点删除事件,子节点删除就意味着锁被释放。
![](https://oss.javaguide.cn/github/javaguide/distributed-system/distributed-lock/distributed-lock-zookeeper.png)
实际项目中,推荐使用 Curator 来实现 ZooKeeper 分布式锁。Curator 是 Netflix 公司开源的一套 ZooKeeper Java 客户端框架,相比于 ZooKeeper 自带的客户端 zookeeper 来说Curator 的封装更加完善,各种 API 都可以比较方便地使用。
`Curator`主要实现了下面四种锁:
- `InterProcessMutex`:分布式可重入排它锁
- `InterProcessSemaphoreMutex`:分布式不可重入排它锁
- `InterProcessReadWriteLock`:分布式读写锁
- `InterProcessMultiLock`:将多个锁作为单个实体管理的容器,获取锁的时候获取所有锁,释放锁也会释放所有锁资源(忽略释放失败的锁)。
```java
CuratorFramework client = ZKUtils.getClient();
client.start();
// 分布式可重入排它锁
InterProcessLock lock1 = new InterProcessMutex(client, lockPath1);
// 分布式不可重入排它锁
InterProcessLock lock2 = new InterProcessSemaphoreMutex(client, lockPath2);
// 将多个锁作为一个整体
InterProcessMultiLock lock = new InterProcessMultiLock(Arrays.asList(lock1, lock2));
if (!lock.acquire(10, TimeUnit.SECONDS)) {
throw new IllegalStateException("不能获取多锁");
}
System.out.println("已获取多锁");
System.out.println("是否有第一个锁: " + lock1.isAcquiredInThisProcess());
System.out.println("是否有第二个锁: " + lock2.isAcquiredInThisProcess());
try {
// 资源操作
resource.use();
} finally {
System.out.println("释放多个锁");
lock.release();
}
System.out.println("是否有第一个锁: " + lock1.isAcquiredInThisProcess());
System.out.println("是否有第二个锁: " + lock2.isAcquiredInThisProcess());
client.close();
```
### 为什么要用临时顺序节点?
每个数据节点在 ZooKeeper 中被称为 **znode**,它是 ZooKeeper 中数据的最小单元。
我们通常是将 znode 分为 4 大类:
- **持久PERSISTENT节点**:一旦创建就一直存在即使 ZooKeeper 集群宕机,直到将其删除。
- **临时EPHEMERAL节点**:临时节点的生命周期是与 **客户端会话session** 绑定的,**会话消失则节点消失** 。并且,**临时节点只能做叶子节点** ,不能创建子节点。
- **持久顺序PERSISTENT_SEQUENTIAL节点**除了具有持久PERSISTENT节点的特性之外 子节点的名称还具有顺序性。比如 `/node1/app0000000001``/node1/app0000000002`
- **临时顺序EPHEMERAL_SEQUENTIAL节点**除了具备临时EPHEMERAL节点的特性之外子节点的名称还具有顺序性。
可以看出,临时节点相比持久节点,最主要的是对会话失效的情况处理不一样,临时节点会话消失则对应的节点消失。这样的话,如果客户端发生异常导致没来得及释放锁也没关系,会话失效节点自动被删除,不会发生死锁的问题。
使用 Redis 实现分布式锁的时候,我们是通过过期时间来避免锁无法被释放导致死锁问题的,而 ZooKeeper 直接利用临时节点的特性即可。
假设不适用顺序节点的话,所有尝试获取锁的客户端都会对持有锁的子节点加监听器。当该锁被释放之后,势必会造成所有尝试获取锁的客户端来争夺锁,这样对性能不友好。使用顺序节点之后,只需要监听前一个节点就好了,对性能更友好。
### 为什么要设置对前一个节点的监听?
> Watcher事件监听器是 ZooKeeper 中的一个很重要的特性。ZooKeeper 允许用户在指定节点上注册一些 Watcher并且在一些特定事件触发的时候ZooKeeper 服务端会将事件通知到感兴趣的客户端上去,该机制是 ZooKeeper 实现分布式协调服务的重要特性。
同一时间段内,可能会有很多客户端同时获取锁,但只有一个可以获取成功。如果获取锁失败,则说明有其他的客户端已经成功获取锁。获取锁失败的客户端并不会不停地循环去尝试加锁,而是在前一个节点注册一个事件监听器。
这个事件监听器的作用是:**当前一个节点对应的客户端释放锁之后也就是前一个节点被删除之后监听的是删除事件通知获取锁失败的客户端唤醒等待的线程Java 中的 `wait/notifyAll` ),让它尝试去获取锁,然后就成功获取锁了。**
### 如何实现可重入锁?
这里以 Curator 的 `InterProcessMutex` 对可重入锁的实现来介绍(源码地址:[InterProcessMutex.java](https://github.com/apache/curator/blob/master/curator-recipes/src/main/java/org/apache/curator/framework/recipes/locks/InterProcessMutex.java))。
当我们调用 `InterProcessMutex#acquire`方法获取锁的时候,会调用`InterProcessMutex#internalLock`方法。
```java
// 获取可重入互斥锁,直到获取成功为止
@Override
public void acquire() throws Exception {
if (!internalLock(-1, null)) {
throw new IOException("Lost connection while trying to acquire lock: " + basePath);
}
}
```
`internalLock` 方法会先获取当前请求锁的线程,然后从 `threadData`( `ConcurrentMap<Thread, LockData>` 类型)中获取当前线程对应的 `lockData``lockData` 包含锁的信息和加锁的次数,是实现可重入锁的关键。
第一次获取锁的时候,`lockData``null`。获取锁成功之后,会将当前线程和对应的 `lockData` 放到 `threadData`
```java
private boolean internalLock(long time, TimeUnit unit) throws Exception {
// 获取当前请求锁的线程
Thread currentThread = Thread.currentThread();
// 拿对应的 lockData
LockData lockData = threadData.get(currentThread);
// 第一次获取锁的话lockData 为 null
if (lockData != null) {
// 当前线程获取过一次锁之后
// 因为当前线程的锁存在, lockCount 自增后返回,实现锁重入.
lockData.lockCount.incrementAndGet();
return true;
}
// 尝试获取锁
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
if (lockPath != null) {
LockData newLockData = new LockData(currentThread, lockPath);
// 获取锁成功之后,将当前线程和对应的 lockData 放到 threadData 中
threadData.put(currentThread, newLockData);
return true;
}
return false;
}
```
`LockData``InterProcessMutex`中的一个静态内部类。
```java
private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();
private static class LockData
{
// 当前持有锁的线程
final Thread owningThread;
// 锁对应的子节点
final String lockPath;
// 加锁的次数
final AtomicInteger lockCount = new AtomicInteger(1);
private LockData(Thread owningThread, String lockPath)
{
this.owningThread = owningThread;
this.lockPath = lockPath;
}
}
```
如果已经获取过一次锁,后面再来获取锁的话,直接就会在 `if (lockData != null)` 这里被拦下了,然后就会执行`lockData.lockCount.incrementAndGet();` 将加锁次数加 1。
整个可重入锁的实现逻辑非常简单,直接在客户端判断当前线程有没有获取锁,有的话直接将加锁次数加 1 就可以了。
基于 ZooKeeper 或者 Redis 实现分布式锁这两种实现方式要用的更多一些,我专门写了一篇文章来详细介绍这两种方案:[分布式锁常见实现方案总结](./distributed-lock-implementations.md)。
## 总结
这篇文章我们介绍了分布式锁的基本概念以及实现分布式锁的两种常见方式。至于具体选择 Redis 还是 ZooKeeper 来实现分布式锁,还是要看业务的具体需求。如果对性能要求比较高的话,建议使用 Redis 实现分布式锁。如果对可靠性要求比较高的话,建议使用 ZooKeeper 实现分布式锁。
这篇文章我们主要介绍了:
- 分布式锁的用途:分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。
- 分布式锁的应该具备的条件:互斥、高可用、可重入、高性能、非阻塞。
- 分布式锁的常见实现方式:关系型数据库比如 MySQL、分布式协调服务 ZooKeeper、分布式键值存储系统比如 Redis 、Etcd 。

View File

@ -1 +0,0 @@
<mxfile host="Electron" modified="2023-03-22T07:46:10.534Z" agent="5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/20.3.0 Chrome/104.0.5112.114 Electron/20.1.3 Safari/537.36" etag="AiToXsPRDCuB0m7tcCvY" version="20.3.0" type="device"><diagram id="OCHATffbN17S1b6ytZqW" name="第 1 页">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</diagram></mxfile>

View File

@ -1 +0,0 @@
<mxfile host="Electron" modified="2022-09-01T05:56:36.359Z" agent="5.0 (Macintosh; Intel Mac OS X 10_16_0) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.4.5 Chrome/83.0.4103.122 Electron/9.1.0 Safari/537.36" etag="rtqWC0t9ZphBIG-VxnhK" version="13.4.5" type="device"><diagram id="1WlLurKbbY4VXUe8oHch" name="第 1 页">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</diagram></mxfile>

View File

@ -1 +0,0 @@
<mxfile host="Electron" modified="2023-02-25T04:22:30.160Z" agent="5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/20.3.0 Chrome/104.0.5112.114 Electron/20.1.3 Safari/537.36" etag="Fg7v1vZiTGKDpKPKZGDF" version="20.3.0" type="device"><diagram id="1WlLurKbbY4VXUe8oHch" name="第 1 页">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</diagram></mxfile>

View File

@ -1 +0,0 @@
<mxfile host="Electron" modified="2022-09-01T02:08:58.162Z" agent="5.0 (Macintosh; Intel Mac OS X 10_16_0) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.4.5 Chrome/83.0.4103.122 Electron/9.1.0 Safari/537.36" etag="NSFpHFTWZMqKqWBPE-A3" version="13.4.5" type="device"><diagram id="1WlLurKbbY4VXUe8oHch" name="第 1 页">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</diagram></mxfile>

View File

@ -1 +0,0 @@
<mxfile host="Electron" modified="2022-09-01T02:08:36.651Z" agent="5.0 (Macintosh; Intel Mac OS X 10_16_0) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/13.4.5 Chrome/83.0.4103.122 Electron/9.1.0 Safari/537.36" etag="rlszdDvRCq1sE68Id5g7" version="13.4.5" type="device"><diagram id="WvAjgkPtNnpgH_JUfW7g" name="第 1 页">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</diagram></mxfile>

View File

@ -71,6 +71,12 @@ CAP 理论的提出者布鲁尔在提出 CAP 猜想的时候,并没有详细
2. **Eureka 保证的则是 AP。** Eureka 在设计的时候就是优先保证 A (可用性)。在 Eureka 中不存在什么 Leader 节点,每个节点都是一样的、平等的。因此 Eureka 不会像 ZooKeeper 那样出现选举过程中或者半数以上的机器不可用的时候服务就是不可用的情况。 Eureka 保证即使大部分节点挂掉也不会影响正常提供服务,只要有一个节点是可用的就行了。只不过这个节点上的数据可能并不是最新的。
3. **Nacos 不仅支持 CP 也支持 AP。**
**🐛 修正(参见:[issue#1906](https://github.com/Snailclimb/JavaGuide/issues/1906)**
ZooKeeper 通过可线性化Linearizable写入、全局 FIFO 顺序访问等机制来保障数据一致性。多节点部署的情况下, ZooKeeper 集群处于 Quorum 模式。Quorum 模式下的 ZooKeeper 集群, 是一组 ZooKeeper 服务器节点组成的集合,其中大多数节点必须同意任何变更才能被视为有效。
由于 Quorum 模式下的读请求不会触发各个 ZooKeeper 节点之间的数据同步因此在某些情况下还是可能会存在读取到旧数据的情况导致不同的客户端视图上看到的结果不同这可能是由于网络延迟、丢包、重传等原因造成的。ZooKeeper 为了解决这个问题,提供了 Watcher 机制和版本号机制来帮助客户端检测数据的变化和版本号的变更,以保证数据的一致性。
### 总结
在进行分布式系统设计和开发时,我们不应该仅仅局限在 CAP 问题上,还要关注系统的扩展性、可用性等等

View File

@ -135,4 +135,4 @@ CPU 缓存是通过将最近使用的数据存储在高速缓存中来实现更
## 参考
- Disruptor 高性能之道-等待策略:<http://wuwenliang.net/2022/02/28/Disruptor高性能之道-等待策略/>
- 《Java 并发编程实战》- 40 | 案例分析(三):高性能队列 Disruptorhttps://time.geekbang.org/column/article/98134
- 《Java 并发编程实战》- 40 | 案例分析(三):高性能队列 Disruptor<https://time.geekbang.org/column/article/98134>

View File

@ -350,7 +350,8 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8](https://docs.oracle.
### 分布式锁
[分布式锁常见知识点&面试题总结](./distributed-system/distributed-lock.md)
- [分布式锁介绍](https://javaguide.cn/distributed-system/distributed-lock.html)
- [分布式锁常见实现方案总结](https://javaguide.cn/distributed-system/distributed-lock-implementations.html)
### 分布式事务

View File

@ -654,10 +654,11 @@ public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
> `TransactionInterceptor` 类中的 `invoke()`方法内部实际调用的是 `TransactionAspectSupport` 类的 `invokeWithinTransaction()`方法。由于新版本的 Spring 对这部分重写很大,而且用到了很多响应式编程的知识,这里就不列源码了。
#### Spring AOP 自调用问题
因为SpringAOP工作原理导致@Transaction失效
当一个方法被标记了@Transactional注解的时候Spring事务管理器只会在被其他类方法调用的时候生效而不会在一个类中方法调用生效。
这是因为Spring AOP工作原理决定的。因为Spring AOP使用动态代理来实现事务的管理他会在运行的时候为带有@Transaction注解的方法生成代理对象,并在方法调用的前后应用事物逻辑。如果该方法被其他类调用我们的代理对象就会拦截方法调用并处理事物。但是在一个类中的其他方法内部调用的时候我们代理对象就无法拦截到这个内部调用,因此事物也就失效了。
当一个方法被标记了`@Transactional` 注解的时候Spring 事务管理器只会在被其他类方法调用的时候生效,而不会在一个类中方法调用生效。
这是因为 Spring AOP 工作原理决定的。因为 Spring AOP 使用动态代理来实现事务的管理,它会在运行的时候为带有 `@Transactional` 注解的方法生成代理对象,并在方法调用的前后应用事物逻辑。如果该方法被其他类调用我们的代理对象就会拦截方法调用并处理事务。但是在一个类中的其他方法内部调用的时候,我们代理对象就无法拦截到这个内部调用,因此事务也就失效了。
`MyService` 类中的`method1()`调用`method2()`就会导致`method2()`的事务失效。