mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-08-14 05:21:42 +08:00
Compare commits
4 Commits
1841062c12
...
b0cb31e2f6
Author | SHA1 | Date | |
---|---|---|---|
|
b0cb31e2f6 | ||
|
c6a5d26e1d | ||
|
29310cd83f | ||
|
703c7b4b75 |
@ -322,6 +322,10 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8 ](https://docs.oracle
|
||||
|
||||
[为什么要分布式 id ?分布式 id 生成方案有哪些?](./docs/distributed-system/distributed-id.md)
|
||||
|
||||
### 分布式锁
|
||||
|
||||
[分布式锁](./docs/distributed-system/distributed-lock.md)
|
||||
|
||||
### 分布式事务
|
||||
|
||||
[分布式事务](./docs/distributed-system/distributed-transaction.md)
|
||||
|
@ -394,6 +394,7 @@ export const sidebarConfig = defineSidebarConfig({
|
||||
},
|
||||
"api-gateway",
|
||||
"distributed-id",
|
||||
"distributed-lock",
|
||||
{
|
||||
text: "RPC",
|
||||
prefix: "rpc/",
|
||||
|
117
docs/distributed-system/distributed-lock.md
Normal file
117
docs/distributed-system/distributed-lock.md
Normal file
@ -0,0 +1,117 @@
|
||||
---
|
||||
title: 分布式锁
|
||||
category: 分布式
|
||||
---
|
||||
|
||||
## 什么是分布式锁?
|
||||
|
||||
对于单机多线程,在 Java 中,我们通常使用 `ReetrantLock` 这类 JDK 自带的 **本地锁** 来控制本地多个线程对本地共享资源的访问。对于分布式系统,我们通常使用 **分布式锁** 来控制多个服务对共享资源的访问。
|
||||
|
||||
一个最基本的分布式锁需要满足:
|
||||
|
||||
- **互斥** :任意一个时刻,锁只能被一个线程持有;
|
||||
- **高可用** :锁服务是高可用的。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。
|
||||
|
||||
通常情况下,我们一般会选择基于 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 分布式锁实现,实现方式比较简单,性能也很高效。不过,这种方式实现分布式锁存在一些问题。就比如应用程序遇到一些问题比如释放锁的逻辑突然挂掉,可能会导致锁无法被释放,进而造成共享资源无法再被其他线程/进程访问。
|
||||
|
||||
### 为什么要给锁设置一个过期时间?
|
||||
|
||||
为了避免锁无法被释放,我们可以想到的一个解决办法就是:给这个 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 。
|
||||
|
||||

|
||||
|
||||
Redisson 是一个开源的 Java 语言 Redis 客户端,提供了很多开箱即用的功能,不仅仅包括多种分布式锁的实现。
|
||||
|
||||
Redisson 中的分布式锁自带自动续期机制,它提供了一个专门用来监控锁的 **Watch Dog( 看门狗)**,如果操作共享资源的还未完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。
|
||||
|
||||
我这里以 Redisson 的分布式可重入锁 `RLock` 为例来说明如何使用 Redisson 实现分布式锁:
|
||||
|
||||
```java
|
||||
// 1.获取指定的分布式锁对象
|
||||
RLock lock = redisson.getLock("lock");
|
||||
// 2.拿锁,具有 Watch Dog 自动续期机制
|
||||
lock.lock();
|
||||
// 3.执行业务
|
||||
...
|
||||
// 4.释放锁
|
||||
lock.unlock();
|
||||
```
|
||||
|
||||
可以看出,代码非常简洁直观。
|
||||
|
||||
如果使用 Redis 来实现分布式锁的话,还是比较推荐直接基于 Redisson 来做的。
|
||||
|
||||
### Redis 如何解决集群情况下分布式锁的可靠性?
|
||||
|
||||
为了避免单点故障,生产环境下的 Redis 服务通常是集群化部署的。
|
||||
|
||||
Redis 集群下,上面介绍到的分布式锁的实现会存在一些问题。由于 Redis 集群数据同步到各个节点时是异步的,如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。
|
||||
|
||||
针对这个问题,Redis 之父 antirez 设计了 [Redlock 算法](https://redis.io/topics/distlock) 来解决。
|
||||
|
||||
Redlock 算法的思想是让客户端向 Redis 集群中的多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
|
||||
|
||||
即使部分 Redis 节点出现问题,只要保证 Redis 集群中有半数以上的 Redis 节点可用,分布式锁服务就是正常的。
|
||||
|
||||
Redlock 是直接操作 Redis 节点的,并不是通过 Redis 集群操作的,这样才可以避免 Redis 集群主从切换导致的锁丢失问题。
|
||||
|
||||
Redlock 实现比较复杂,性能比较差,发生时钟变迁的情况下还存在安全性隐患。《数据密集型应用系统设计》一书的作者 Martin Kleppmann 曾经专门发文怼过 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 来做,只是性能会差一些。
|
@ -5,17 +5,11 @@ tag:
|
||||
- 分布式协议&算法
|
||||
---
|
||||
|
||||
经历过技术面试的小伙伴想必对这个两个概念已经再熟悉不过了!
|
||||
经历过技术面试的小伙伴想必对 CAP & BASE 这个两个理论已经再熟悉不过了!
|
||||
|
||||
Guide哥当年参加面试的时候,不夸张地说,只要问到分布式相关的内容,面试官几乎是必定会问这两个分布式相关的理论。
|
||||
我当年参加面试的时候,不夸张地说,只要问到分布式相关的内容,面试官几乎是必定会问这两个分布式相关的理论。一是因为这两个分布式基础理论是学习分布式知识的必备前置基础,二是因为很多面试官自己比较熟悉这两个理论(方便提问)。
|
||||
|
||||
并且,这两个理论也可以说是小伙伴们学习分布式相关内容的基础了!
|
||||
|
||||
因此,小伙伴们非常非常有必要将这理论搞懂,并且能够用自己的理解给别人讲出来。
|
||||
|
||||
这篇文章我会站在自己的角度对这两个概念进行解读!
|
||||
|
||||
*个人能力有限。如果文章有任何需要改善和完善的地方,欢迎在评论区指出,共同进步!——爱你们的Guide哥*
|
||||
我们非常有必要将这两个理论搞懂,并且能够用自己的理解给别人讲出来。
|
||||
|
||||
## CAP理论
|
||||
|
||||
@ -37,11 +31,11 @@ CAP 理论的提出者布鲁尔在提出 CAP 猜想的时候,并没有详细
|
||||
|
||||
- **一致性(Consistency)** : 所有节点访问同一份最新的数据副本
|
||||
- **可用性(Availability)**: 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。
|
||||
- **分区容错性(Partition tolerance)** : 分布式系统出现网络分区的时候,仍然能够对外提供服务。
|
||||
- **分区容错性(Partition Tolerance)** : 分布式系统出现网络分区的时候,仍然能够对外提供服务。
|
||||
|
||||
**什么是网络分区?**
|
||||
|
||||
> 分布式系统中,多个节点之前的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫网络分区。
|
||||
分布式系统中,多个节点之前的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫 **网络分区**。
|
||||
|
||||

|
||||
|
||||
|
@ -325,6 +325,10 @@ JVM 这部分内容主要参考 [JVM 虚拟机规范-Java8 ](https://docs.oracle
|
||||
|
||||
[为什么要分布式 id ?分布式 id 生成方案有哪些?](./distributed-system/distributed-id.md)
|
||||
|
||||
### 分布式锁
|
||||
|
||||
[分布式锁](./distributed-system/distributed-lock.md)
|
||||
|
||||
### 分布式事务
|
||||
|
||||
[分布式事务](./distributed-system/distributed-transaction.md)
|
||||
|
@ -457,9 +457,12 @@ public class Thread implements Runnable {
|
||||
|
||||
```java
|
||||
public void set(T value) {
|
||||
//获取当前请求的线程
|
||||
Thread t = Thread.currentThread();
|
||||
//取出 Thread 类内部的 threadLocals 变量(哈希表结构)
|
||||
ThreadLocalMap map = getMap(t);
|
||||
if (map != null)
|
||||
// 将需要存储的值放入到这个哈希表中
|
||||
map.set(this, value);
|
||||
else
|
||||
createMap(t, value);
|
||||
|
Loading…
x
Reference in New Issue
Block a user