分布式锁的实现:Redis和Zookeeper
zookeeper是一个分布式协调服务
分布式协调技术
分布式协调技术是用来解决分布式环境下多个进程之间的同步控制,让他们有序的访问某些临界资源,防止造成脏数据的后果
本质就是分布式锁
Zookeeper就是实现分布式锁的实现
多个订单服务,同时下订单,一共只有5个商品,如何防止超卖问题
订单服务进程之间的问题,就是需要实现分布式锁
分布式锁应具备的条件
- 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个进程进行处理
- 高可用的获取锁和释放锁
- 高性能的获取锁和释放锁
- 具体锁失效机制,防止死锁
- 具备非阻塞锁特性,如果没有获取锁,就直接返回获取锁失败
分布式锁的实现有哪些
- Redis
Redis需要手动处理,利用Redis的setnx命令,该命令是原子性的,只有在key不存在的情况下,才能set成功 - Zookeeper
天生就是为了实现分布式锁的,利用Zookeeper的顺序临时节点,来实现分布式锁和等待队列
Zookeeper设计的初衷,就是为了实现分布式锁
通过Redis分布式锁的实现理解基础概念
分布式锁有三个核心概念
加锁
使用setnx
命令,key是锁的唯一标识,按业务来决定命名,比如对商品秒杀活动,key可以是lock_sale_商品ID,value可以设置为1
setnx(lock_sale_商品Id,1)
当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到锁;当一个线程执行setnx,返回0,说明key已经存在,该线程抢锁失败
解锁
有加锁,就有解锁,当得到锁的线程执行完成之后,就需要释放锁,以便其他线程可以进入,redis最简单的就是使用del
命令
del(lock_sale_商品Id)
释放锁之后,其他线程可以继续执行setnx来获取锁
锁超时
如果一个得到锁的线程,在执行任务的过程中挂掉,来不及释放锁,就会产生死锁,别的线程也进不来,所以setnx就需要一个超时时间,setnx不支持超时参数,就需要额外的指令
expire(lock_sale_商品Id,30)
伪代码如下:
if(setnx(lock_sale_商品Id,1) == 1){
expire(lock_sale_商品Id,30);
try{
do something()
}finally{
del(lock_sale_商品Id)
}
}
存在的问题
setnx
和expire
非原子性的
在线程得到了锁,还没有执行到expire时就挂掉了,就会产生死锁
解决办法:使用set(lock_sale_商品Id,1,30,nx)
取代setnx,可以解决del
导致误删
极端场景,一个线程得到锁,设置了超时时间是30秒,但是执行的比较慢,30秒还没有执行完成,锁就过期了,另一个线程获取到了锁,此时第一个线程执行完成,去删除锁,删除的是另外一个线程加的锁
解决办法:判断是否是自己加的锁,set的value值可以设置为线程Id,这样就可以判断是否是自己加的锁,不是则不删除- 第2个问题完美解决方案
给获得锁的线程增加一个守护线程,给快到期的锁续航
Zookeeper的数据模型
Zookeeper的数据模型,很像数据结构中的树,也像文件系统的目录
树有节点组成,zookeeper的数据存储也同样是基于节点的,这个节点叫做Znode
不过,不同于树的节点,Znode的引用是路径引用,类似于文件路径
/动物/狗
/汽车/宝马
这样的层次结构,让每个Znode拥有一个唯一的路径
Znode包含哪些数据
data:Znode存储的数据信息
ACL:记录Znode的访问权限,即哪些人或哪些IP可以访问本节点
stat:包含Znode的各种元数据,比如事务Id,版本号,时间戳,大小等
child:当前节点的子节点引用
Zookeeper是为读多写少的场景设计的, 每个节点的数据不能超过1MB
Watch的概念–事件通知
Zookeeper的基本操作:
创建节点:create
删除节点:delete
是否存在:exist
获取数据:getData
设置数据:setData
获取所有子节点:getChildren
其中的exist,getData,getChildren都是读操作,Zookeeper在请求读操作的时候,可以选择是否设置Watch
可以把Watch理解成是注册在Znode上的触发器,当这个Znode发生改变的时候,也就是调用了create,delete,setData方法的时候,将会触发Znode上注册的对应事件,请求Watch的客户端会接受到异步通知
具体交互过程:
客户端调用getData方法,watch参数是true,服务端接收到请求,返回节点数据,并且在对应的哈希表里插入被Watch的Znode路径,以及Watcher列表
当被Watch的Znode被删除时,服务器端会查找哈希表,找到该Znode对应的所有Watcher,异步通知客户端,并且删除哈希表中对应的key-value
Zookeeper的一致性
一个Zookeeper集群,部署多个Zookeeper节点,每个节点的数据需要进行同步,就是数据一致性那问题
如何解决数据一致性
Zookeeper Server是一主多从结构
在更新数据时,首先更新到主节点,再同步到从节点
在读取数据时,直接读取任意从节点
为了保证数据的一致性,Zookeeper采用了ZAB
协议,这种协议非常类似于一致性算法Paxos和Raft.
什么是ZAB协议
Zookeeper Atomic Broadcast 可以解决两个问题
- 集群崩溃恢复
- 主从同步数据
ZAB协议定义的三种节点状态
- Looking :选举状态
- Following:Follower节点所处的状态
- Leading:Leader节点所处的状态
最大ZXID
最大ZXID也就是节点本地最新事务编号,可以理解为自增id
每个节点都有一个ZXID,谁的ZXID大,谁的数据就是最新的
ZAB的崩溃恢复
当Zookeeper的主节点崩溃了,集群会进行崩溃恢复,ZAB的崩溃恢复会进行三个阶段:
Leader election
选举阶段,此时集群的节点处于Looking阶段,它们会各自向其他节点发起投票,投票当中包含自己的服务器ID和最新的事务ID(ZXID)
接下来,节点就会用自身的ZXID和从其他节点接收到的ZXID做比较,如果发现别人家的ZXID比自己的大,也就是数据比自己的新,就重新发起投票,投票给目前已知的最大的ZXID所有节点
每次投票,服务器都会统计投票数量,判断是否有节点得到半数以上的投票,如果有这样的节点,该节点将会成为准Leader节点,就是Leading,其他节点的状态为Following
Discover
发现阶段,用于从节点中发现最新的ZXID和事务日志
问题:既然第一个阶段,Leader被选为主节点,已经是集群中数据最新的了, 为什么还要从集群中寻找最新事务呢?
答案:为了防止因为网络问题,在上一阶段出现多个Leader的情况
在这个阶段,Leader接收所有Following发来的各自的epoch值,Leader从中选出最大的epoch值,加1,返回给所有的Following
各个Following收到全新的epoch后,返回ACK给Leader,带上各自的ZXID和最新事务日志,Leader选出最大的ZXID,并更新事务日志
Synchronization
同步阶段,把Leader接收到的最新事务日志,同步给集群中所有的Following,只有当半数Following同步成功,这个准Leader才会成为Leader
ZAB的数据恢复
Broadcast:广播
常规情况下更新数据的时候,由Leader广播到所有的Following
- 客户端发出写入请求给任意的Following
- Following把写入请求转发给Leader节点
- Leader采用二阶段提交的方法,先发送Propose广播给Following
- Following接收到Propose,写入日志成功后,返回ACK消息给Leader
- Leader接收到半数以上的ACK,返回成功给客户端,并且广播Commit请求给Following
ZAB协议即不是强一致性,也不是弱一致性,而是顺序一致性,它依靠事务ID和版本号,保证了数据的更新和读取是有序的
Zookeeper分布式锁
什么是临时顺序节点
Znode分为四种类型:
持久节点,持久节点顺序节点
临时节点,临时顺序节点
临时节点:当创建节点的客户端与Zookeeper断开连接后,临时节点会被删除
临时顺序节点:在创建节点时,Zookeeper根据创建的时候顺序给该节点名称进行编号;当创建节点的客户端与Zookeeper断开连接后,临时节点会被删除
Zookeeper分布式锁原理
Zookeeper分布式锁应用了临时顺序节点,来实现,看下详细步骤:
- 获取锁
首先,在Zookeeper当中创建一个持久节点ParentLock,当第一个客户端想要获得锁时,就需要在ParentLock这个节点下面创建一个临时顺序节点Lock1
之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个,如果是第一个节点,则成功获得锁
这时候,如果再有一个客户端Client2来获取锁,则在ParentLock再创建一个顺序临时节点Lock2
Client2查找ParentLock下面所有顺序临时节点并排序,判断自己创建的Lock2是不是排名最靠前的一个,结果发现Lock2并不是最靠前的
于是,Client2就排序向仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在,意味这此时Lock2抢锁失败,进入到等待状态
这时候,如果又有一个客户端Client3前来获取锁,则在ParentLock下再创建一个临时顺序节点Lock3
Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己创建的Lock3是否是排名最靠前的,发现不是,Client3就向排序仅比它靠前的Lock2注册Watcher,用于监听Lock2是否存在,Client3也抢锁失败,进入到了等待状态
这样,Client1得到锁,Client2和Client3等待状态
- 释放锁
释放锁分为两种
- 任务完成,客户端释放锁
当任务完成时,Client1会显示调用删除节点Lock1的指令 - 任务执行过程中,客户端崩溃
获得锁的Client1,如果崩溃,与Zookeeper服务端的连接断开,根据临时顺序节点的特性,相关联的Lock1会自动删除
此时由于Client2一直监听这Lock1的存在状态,当Lock1被删除,Client2会立即收到通知,这个时候,Client2会再次查询ParentLock下面的所有节点,确认自己的Lock2节点是否是排名最靠前的,如果是,则获取到锁
Zookeeper和Redis分布式锁的比较