# 1.为什么要使用分布式锁
使用分布式锁的目的,无外乎就是保证同一时间只有
一个客户端可以对共享资源进行操作。
## 1.1举
一个很长的例子
系统 A 是
一个电商系统,目前是一台机器部署,系统中有
一个用户下订单的接口,但是
用户下订单之前一定要去检查一下库存,确保库存足够了才会给
用户下单。由于系统有一定的并发,所以会预先将商品的库存保存在 Re
dis 中,
用户下单的时候会更新 Re
dis 的库存。此时系统架构如下:

但是这样一来会产生
一个问题:假如某个时刻,Re
dis 里面的某个商品库存为 1。
此时两个请求同时到来,其中
一个请求执行到上图的第 3 步,更新
数据库的库存为 0,但是第 4 步还没有执行。
而另外
一个请求执行到了第 2 步,发现库存还是 1,就继续执行第 3 步。这样的结果,是导致卖出了 2 个商品,然而其实库存只有 1 个。
很明显不对啊!这就是典型的**库存超卖问题**。此时,我们很容易想到
解决方案:用锁把 2、3、4 步锁住,让他们执行完之后,另
一个线程才能进来执行第 2 步。

按照上面的图,在执行第 2 步时,使用 Java 提供的 Synchronized 或者
reentrantlock 来锁住,然后在第 4 步执行完之后才释放锁。
这样一来,2、3、4 这 3 个步骤就被“锁”住了,多个线程之间只能**串行化执行**。
当整个系统的并发飙升,一台机器扛不住了。现在要
增加一台机器,如下图:

增加机器之后,系统变成上图所示,假设此时两个
用户的请求同时到来,但是落在了不同的机器上,那么这两个请求是可以同时执行了,还是会出现库存超卖的问题。
因为上图中的两个 A 系统,运行在两个不同的 JVM 里面,他们加的锁只对属于自己 JVM 里面的线程有效,对于其他 JVM 的线程是无效的。
因此,这里的问题是:Java 提供的原生锁机制在多机部署场景下失效了,这是因为两台机器加的锁不是同
一个锁(两个锁在不同的 JVM 里面)。
那么,我们只要保证两台机器加的锁是同
一个锁,问题不就
解决了吗?此时,就该分布式锁隆重登场了。
分布式锁的思路是:**在整个系统提供
一个全局、唯一的
获取锁的“东西”,然后每个系统在需要加锁时,都去问这个“东西”拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。**
至于这个“东西”,可以是 Re
dis、Zookeeper,也可以是
数据库。此时的架构如图:

通过上面的分析,我们知道了库存超卖场景在分布式部署系统的情况下使用 Java 原生的锁机制无法保证线程安全,所以我们需要用到分布式锁的方案。
# 2.高效的分布式锁
在设计分布式锁的时候,应该考虑分布式锁至少要满足的一些条件,同时考虑如何高效的设计分布式锁,以下几点是必须要考虑的:
**(1)** **互斥**
在分布式高并发的条件下,最需要保证在同一时刻只能有
一个线程获得锁,这是最基本的一点。
**(2)** **防止死锁**
在分布式高并发的条件下,比如有个线程获得锁的同时,还没有来得及去释放锁,就因为系统故障或者其它原因使它无法执行释放锁的命令,导致其它线程都无法获得锁,造成**死锁**。所以分布式非常有必要设置锁的有效时间,确保系统出现故障后,在一定时间内能够主动去释放锁,避免造成死锁的情况。
**(3)** **
性能**
对于访问量大的共享资源,需要考虑减少锁等待的时间,避免导致大量线程阻塞。
所以在锁的设计时,需要考虑两点。
1、 锁的颗粒度要尽量小。比如你要通过锁来减库存,那这个锁的
名称你可以设置成是商品的ID,而不是任取
名称。这样这个锁只对当前商品有效,锁的颗粒度小。
2、 锁的范围尽量要小。比如只要锁2行
代码就可以
解决问题的,那就不要去锁10行
代码了。
**(4)** **重入**
我们知道
reentrantlock是可重入锁,那它的特点就是:同
一个线程可以重复拿到同
一个资源的锁。重入锁非常有利于资源的高效利用。关于这点之后会做演示。
# 3.基于Re
dis实现分布式锁
## 3.1 使用Re
dis命令实现分布式锁
### 3.1.1加锁
加锁实际上就是在re
dis中,给Key键设置
一个值,为避免死锁,并给定
一个过期时间。
使用的命令**:SET lock_key random_value NX PX 5000**
值得注意的是:
random_value 是客户端
生成的唯一的字符串。
NX 代表只在键不存在时,才对键进行设置操作。
PX 5000 设置键的过期时间为5000毫秒。
也可以使用另外一条命令:**SETNX key value**
只不过过期时间无法设置。
这样,如果上面的命令执行成功,则证明客户端
获取到了锁。
### 3.1.2解锁
解锁的过程就是将Key键
删除,但要保证安全性,举个例子:客户端1的请求不能将客户端2的锁给
删除掉。
释放锁涉及到两条指令,这两条指令不是原子性的,需要用到re
dis的lua脚本
支持特性,re
dis执行lua脚本是原子性的。脚本如下:
```
if re
dis.call('get',KEYS[1]) == ARGV[1] then
return re
dis.call('del',KEYS[1])
else
return 0
end
```
这种方式比较简单,但是也有
一个最重要的问题:**锁不具有可重入性**。
## 3.2使用Re
disson实现分布式锁
### 3.2.1Re
disson介绍
Re
disson是架设在Re
dis基础上的
一个Java驻内存数据网格(In-Memory Data Grid)。充分的利用了Re
dis键值
数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之
间的协作。
### 3.2.2Re
disson简单使用
```
Con
fig con
fig = new Con
fig();
con
fig.useClusterServers()
.addNodeAddress("re
dis://192.168.31.101:7001")
.addNodeAddress("re
dis://192.168.31.101:7002")
.addNodeAddress("re
dis://192.168.31.101:7003")
.addNodeAddress("re
dis://192.168.31.102:7001")
.addNodeAddress("re
dis://192.168.31.102:7002")
.addNodeAddress("re
dis://192.168.31.102:7003");
Re
dissonClient re
disson = Re
disson.create(con
fig);
RLock lock = re
disson.getLock("anyLock");
lock.lock();
lock.unlock();
```
只需要通过它的 API 中的 Lock 和 Unlock 即可完成分布式锁,而且考虑了很多细节:
l Re
disson 所有指令都通过 Lua 脚本执行,Re
dis 支持 **Lua 脚本原子性执行**。
l Re
disson 设置
一个 Key 的
默认过期时间为 30s,但是如果
获取锁之后,会有
一个WatchDog每隔10s将key的超时时间设置为30s。
另外,Re
disson 还提供了对 Redlock 算法的
支持,它的
用法也很简单:
```
Re
dissonClient re
disson = Re
disson.create(con
fig);
RLock lock1 = re
disson.getFairLock("lock1");
RLock lock2 = re
disson.getFairLock("lock2");
RLock lock3 = re
disson.getFairLock("lock3");
Re
dissonRedLock multiLock = new Re
dissonRedLock(lock1, lock2, lock3);
multiLock.lock();
multiLock.unlock();
```
### 3.2.3Re
disson原理分析

#### (1) 加锁机制
线程去
获取锁,
获取成功: 执行lua脚本,保存数据到re
dis数据库。
线程去
获取锁,
获取失败: 一直通过while循环尝试
获取锁,
获取成功后,执行lua脚本,保存数据到re
dis数据库。
#### (2) WatchDog
自动延期机制
在
一个分布式环境下,假如
一个线程获得锁后,突然服务器宕机了,那么这个时候在一定时间后这个锁会
自动释放,也可以设置锁的有效时间(不设置
默认30秒),这样的目的主要是防止死锁的发生。但是在实际情况中会有一种情况,业务处理的时间可能会大于锁过期的时间,这样就可能**导致解锁和加锁不是同
一个线程。**所以WatchDog作用就是Re
disson实例
关闭前,不断延长锁的有效期。
如果程序
调用加锁
方法显式地给了有效期,是不会开启
后台线程(也就是watch dog)进行延期的,如果没有给有效期或者给的是-1,re
disson会
默认设置30s有效期并且会开启
后台线程(watch dog)进行延期
多久进行一次延期:**(
默认有效期/3)**,
默认有效期可以设置
修改的,即
默认情况下每隔10s设置有效期为30s
#### (3) 可重入加锁机制
Re
disson可以实现可重入加锁机制的原因:
l Re
dis存储锁的数据类型是Hash类型
l Hash数据类型的key值包含了当前线程的信息
下面是re
dis存储的数据

这里表面数据类型是Hash类型,Hash类型相当于我们java的 <key,<key1,value>> 类型,这里key是指 're
disson'
它的有效期还有9秒,我们再来看里们的key1值为078e44a3-5f95-4e24-b6aa-80684655a15a:45它的组成是:
guid + 当前线程的ID。后面的value是就和可重入加锁有关。value代表同一客户端
调用lock
方法的
次数,即可重入计数
统计。
**举图说明**

上面这图的意思就是可重入锁的机制,它最大的优点就是相同线程不需要在等待锁,而是可以直接进行相应操作。
### 3.2.4
获取锁的流程

其中的指定字段也就是hash结构中的field值(构成是uuid+线程id),即判断锁是否是当前线程
### 3.2.5 加锁的流程

### 3.2.6 释放锁的流程

# 4\. 使用Re
dis做分布式锁的缺点
Re
dis有三种部署方式
l 单机模式
l Master-Slave+Sentienl选举模式
l Re
dis Cluster模式
如果采用单机部署模式,会存在单点问题,只要 Re
dis 故障了。加锁就不行了
采用 Master-Slave 模式,加锁的时候只对
一个节点加锁,即便通过 Sentinel 做了高可用,但是如果 Master 节点故障了,发生主从切换,此时就会有可能出现锁丢失的问题。
基于以上的考虑,Re
dis 的作者也考虑到这个问题,他提出了
一个 RedLock 的算法。
这个算法的意思大概是这样的:假设 Re
dis 的部署模式是 Re
dis Cluster,总共有 5 个 Master 节点。
通过以下步骤
获取一把锁:
*
获取当前时间戳,单位是毫秒。
* 轮流尝试在每个 Master 节点上创建锁,过期时间设置较短,一般就几十毫秒。
* 尝试在大多数节点上建立
一个锁,比如 5 个节点就要求是 3 个节点(n / 2 +1)。
* 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了。
* 要是锁建立失败了,那么就依次
删除这个锁。
* 只要别人建立了一把分布式锁,你就得不断轮询去尝试
获取锁。
但是这样的这种算法,可能会出现**节点崩溃重启,多个客户端持有锁**等其他问题,无法保证加锁的过程一定正确。例如:
假设一共有5个Re
dis节点:A, B, C, D, E。设想发生了如下的事件序列:
(1)客户端1成功锁住了A, B, C,
获取锁成功(但D和E没有锁住)。
(2)节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了。
(3)节点C重启后,客户端2锁住了C, D, E,
获取锁成功。
这样,客户端1和客户端2同时获得了锁(针对同一资源)。
### 最后
> **笔者已经把面试题和答案整理成了面试专题文档,有想
获取到借鉴参考的朋友:点赞关注后,[戳这里即可免费领取](https://docs.qq.com/doc/DSmxTbFJ1cmN1R2dB)**

?

?

?
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。