文章目录
- 引言
- 正文
- 分布式锁的定义
- 分布式锁的具体应用场景
- 如何实现分布式锁
- 主动轮询型分布式锁
- 实现思路
- 一、MySQL分布式锁
- 二、Redis分布式锁
- 监听回调型分布式锁
- Etcd分布式锁
- Zookeeper分布式锁
- 锁的对比
- 总结
引言
- 最近面试,一直被问到分布式锁,然后仅仅只知道redis,了解的不够深刻,这里做一个深入的总结
正文
分布式锁的定义
-
在Java多线程编程中,使用Synchronized来实现多个线程之间有序地访问共享资源。相似地定义,分布式锁针对多个机器而言的,控制他们有序访问共享的资源节点。
-
一般是通过第三方组件,控制多个机器有序访问,常见的有
- MySQL
- Redis
- Etcd
- Zookeeper
-
基于redis中的分布式锁的命令“setnx key value px expire_time”,分布式锁具有如下性质
- 独占性:同一把锁,某一个时刻只能被一个机器占用。
- 健壮性:不会轻易死锁,如果持有锁的机器偶然死机了,也能够通过过期时间进行兜底,顺利解锁,其他机器持有锁
- 对称性:谁加锁,谁解锁,就像redis中的value是每一个线程的uuid一样。
分布式锁的具体应用场景
多任务调度场景
- 多个任务调度器在不同节点上运行,从任务队列中取出任务执行
- 分布式锁能够保证每一个任务都只会被调用一次
电子商务场景
- 电子商务系统中,创建订单的操作会涉及检查库存、支付服务以及物流服务等多个阶段,通过分布式锁,保证这些服务按照指定的顺序执行,保证数据的一致性。
如何实现分布式锁
- 分布式锁的实现模型主要分为两种
- 主动轮询型:
- 取锁方会持续地向分布式锁服务发出获取锁的动作,如果锁已经被占用了,会不断发起轮询请求,直到取到锁为主
- 常见类型
- MySQL、Redis
- 监听回调型
- 在取锁方发现锁已经被其他对象占用时,会创建watcher(监听器)来订阅锁的释放事件,随后不主动获取锁
- 当锁被释放的时候,取锁方能通过之前创建的watcher监听到这一变化,发起竞争锁的尝试动作。
- 常见类型
- Etcd、Zookeeper
- 主动轮询型:
主动轮询型分布式锁
实现思路
使用一个数据变量来表示锁,redis中类似一个String对象,具体流程如下
-
创建一个变量lock表示一把锁,lock是锁的名称,lock的内容是持有锁的具体对象
- 加锁,创建lock对象,并将自身的标识写入到lock中
- 解锁,删除lock对象
- 轮询锁,如果锁已经被其他人获取,就持续性轮询,判定这个变量还存不存在,直到自己持有锁
-
常见的有两种方式
- redis
- MySQL
一、MySQL分布式锁
-
MySQL加入数据时,需要满足唯一性约束,如果满足插入成功,不满足插入不成功。基于此,可以尝试将限定访问的方法设置为具有唯一性约束的数据
- 加锁:插入名称为方法名的记录
- 解锁:删除名称为方法名的记录
- 轮询锁: 检查对应字段是否存在
-
具体实现方法如下
CREATE TABLE 'LOCK_INFO'(
'ID' INT(11) UNSINGNED NOT NULL AUTO_INCREMENT,
'METHOD_NAME' VARCHAR(64) NOT NULL ,
PRIMARY KEY('ID'),
UNIQUE KEY 'METHOD_NAME' // 这个最关键,指明访问的互斥资源对应字段是独一无二的
)
- 竞争锁的方式如下
INSERT INTO LOCK_INFO (NAME) VALUES('LOCKMETHODS');
缺点
-
运行缓慢
- 数据库操作的是磁盘,是通过磁盘IO来实现数据的读写,在高并发的情况下,运行缓慢
-
容易死锁
- 并不能像redis一样,有px,可以给每一个锁设置一个过期时间进行兜底。如果持有锁的线程挂掉了,没有删除数据库中的锁记录,就会导致死锁。
-
单点依赖性
- 数据库是单点,非常依赖数据库的可用性
基本上不用,运行慢而且死锁难以解决,还是会用基于内存存储的组件来实现分布式锁
二、Redis分布式锁
- 不同于MySQL,redi是基于内存的存储系统,轻便高效,并且redis是使用单线程模型来完成主要工作的。
- redis的setnx命令
- ex:可以设置过期时间,以及自身也有过期键删除策略,所以能够有效避免死锁问题
- nx:nx仅仅是目标不存在时,才会设置对象,所以轻松实现分布式锁
具体实现流程
- 每一把锁,使用同一数据来标识,key就是锁的名字
- 加锁:向redis中插入一条key为锁的名字的数据,表示加锁成功
- 解锁:删除名称为key的记录,表示解锁成功
- 轮询:在加锁的时候,发现数据已经存在,就轮询等待,直到持有锁的一方释放锁
- 正常情况下,会间隔一段时间在尝试获取锁
健壮性的实现——锁能够顺利传下去
-
工作站崩溃,无限期持有锁
- 使用ex指令,超过了过期时间,会自动释放锁,防止线程因为持有锁而陷入死锁状态
-
工作没干完就过期了
- 看门狗机制
- 单独创建一个线程,定时向redis进行续期操作,防止业务完成之前,锁就过期了
- 某一个线程长期持有锁,会让其他工作站拿不到锁
- 看门狗机制
对称性的实现——谁加锁谁释放
- Lua脚本
- 使用Lua脚本保证,判断锁的归属权 + 删除锁 操作的原子性
- 使用Lua脚本保证,判断锁的归属权 + 删除锁 操作的原子性
高可用保障——RedLock
-
redis 本身单点执行的,为了应对单点故障,redis支持主从复制,但是redis的主从复制是异步的,这就会导致锁的独占性问题。
- 多个对象同时获得一把锁情况
- 对象A在Master中获得了lock锁,但在之后Master立刻崩溃,但是Slave还没来的及将锁同步过去
- slave提升为Master,但是其中并没有对象A关于lock锁的获取情况,所以对象B在申请lock,也会成功
- 使得对象A和对象B同时获得了lock
- 多个对象同时获得一把锁情况
-
RedLock多个节点的锁
- 客户端会和多个独立的Redis实例依次请求加锁,只要获得半数一样的锁,就算是加锁成功,这样不用担心崩溃问题了。
- 只要多数redis节点正常工作,分布式锁就能正常工作,提升分布式锁的可靠性。
-
锁获取失败
- 锁获取失败之后,会释放已经获得的部分锁,并等待一段随机时间后,在重试获得锁
监听回调型分布式锁
- 不用轮询的,相当于给管理员一个通知方式,有锁可以竞争,就直接通知需要竞争锁的对象来竞争锁。
- 加锁解锁和之前轮询的差不多,唯一的区别就是不用一直轮询,具体流程如下
- 定义锁:一把锁,用一个数据来标识
- 加锁:在存储组件里面插入一条数据,这条数据之前不存在,插入成功,加锁成功
- 解锁:将该条数据从存储组件中删除的行为是解锁操作
最大的区别
- 加锁失败监听
- 锁的竞争者回去监听锁的删除事件
- 当发生锁的删除事件之后,锁的竞争者会继续尝试获取锁。
常见应用
- Etcd分布式锁
- ZooKeeper
Etcd分布式锁
Etcd是一种key-value的基于内存的分布式存储仓库,其实分布式锁的方式如下。
健壮性——锁能够顺利流转
-
租约机制
- 可以为保存锁的key-value设置租约,即使持有锁的对象出现故障,也能够到期自动释放锁,删除键值对
- 支持续约,相当于redis中专门实现的一个看门狗续期线程
-
watch机制
- 支持watch某个固定的key或者watch一个范围的key
- 一旦某一个key或者范围发生变化,客户端会收到通知
总结
- Etcd是基于Raft协议实现的高可靠、强一致性的存储组件,正常情况下不会像Redis主从异步复制一样导致出现所数据丢失的情况
- Etcd是监听回调型分布式锁,基于Watch机制,能够感知锁的释放,再去拿锁,不会像redis一样轮询
Zookeeper分布式锁
- 用于提供分布式引用程序协调服务的组件
- Zookeeper是一种树型结构,具有固定的根节点/,只能在根节点下创建子节点,并在子节点下继续创建节点,只能用绝对路径查询Zookeeper节点。
- 总共有4种类型的节点
- 持久节点:
- 一直存在于Zookeeper中的节点,默认的接待你
- 持久顺序节点:
- 创建节点时,按照创建节点的顺序对节点进行编号
- 临时节点(分布式锁实现)
- 客户端和ZooKeeper连接时临时创建的节点,客户端断开俩节后,进程创建的临时节点就会被删除
- 依赖定期的心跳检测来实现
- 临时顺序节点
- 按时间顺序编号创建的临时节点
- 持久节点:
具体实现流程
-
加锁
- 创建临时顺序节点,不用担心续期的问题,会有心跳检测,直到客户端完成任务才会断开
- 当客户机宕机后,临时节点就会随之消亡
-
解锁
- 客户机完成任务后,主动断开链接,临时节点会消亡。
-
监听
- 监听上一个节点的删除事件
锁的对比
总结
- 这里再说分布式锁的选型的时候,就可以大概从性能和可靠性的角度介绍一下,然后在选择redis,因为我的项目确实对于性能的要求更高,对于可靠性的要求并不高,而且占据任务这个唯一需要分布式锁控制访问的接口,访问时间也很快,不会出现执行时间过长,分布式锁过期的情况。
- 两个角度
- 业务本身:执行时间很短、对于安全性和可靠性的要求并不高(任务本身是幂等的),对于性能要求的高,
- 分布式锁的特性:redis是上述四种锁中性能最高的,而且比较熟悉