分布式锁的实现:Redis和Zookeeper

分布式锁的实现:Redis和Zookeeper

zookeeper是一个分布式协调服务

分布式协调技术

分布式协调技术是用来解决分布式环境下多个进程之间的同步控制,让他们有序的访问某些临界资源,防止造成脏数据的后果

本质就是分布式锁

Zookeeper就是实现分布式锁的实现

多个订单服务,同时下订单,一共只有5个商品,如何防止超卖问题
订单服务进程之间的问题,就是需要实现分布式锁

分布式锁应具备的条件

  • 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个进程进行处理
  • 高可用的获取锁和释放锁
  • 高性能的获取锁和释放锁
  • 具体锁失效机制,防止死锁
  • 具备非阻塞锁特性,如果没有获取锁,就直接返回获取锁失败

分布式锁的实现有哪些

  1. Redis

    Redis需要手动处理,利用Redis的setnx命令,该命令是原子性的,只有在key不存在的情况下,才能set成功

  2. 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)
    }
}

存在的问题
  1. setnxexpire非原子性的

    在线程得到了锁,还没有执行到expire时就挂掉了,就会产生死锁

    解决办法:使用set(lock_sale_商品Id,1,30,nx)取代setnx,可以解决

  2. del导致误删

    极端场景,一个线程得到锁,设置了超时时间是30秒,但是执行的比较慢,30秒还没有执行完成,锁就过期了,另一个线程获取到了锁,此时第一个线程执行完成,去删除锁,删除的是另外一个线程加的锁

    解决办法:判断是否是自己加的锁,set的value值可以设置为线程Id,这样就可以判断是否是自己加的锁,不是则不删除

  3. 第2个问题完美解决方案

    给获得锁的线程增加一个守护线程,给快到期的锁续航

Zookeeper的数据模型

Zookeeper的数据模型,很像数据结构中的树,也像文件系统的目录

img

树有节点组成,zookeeper的数据存储也同样是基于节点的,这个节点叫做Znode

不过,不同于树的节点,Znode的引用是路径引用,类似于文件路径

/动物/狗
/汽车/宝马

这样的层次结构,让每个Znode拥有一个唯一的路径

Znode包含哪些数据

img

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列表

img

当被Watch的Znode被删除时,服务器端会查找哈希表,找到该Znode对应的所有Watcher,异步通知客户端,并且删除哈希表中对应的key-value

img

Zookeeper的一致性

一个Zookeeper集群,部署多个Zookeeper节点,每个节点的数据需要进行同步,就是数据一致性那问题

如何解决数据一致性

img

Zookeeper Server是一主多从结构

在更新数据时,首先更新到主节点,再同步到从节点

在读取数据时,直接读取任意从节点

为了保证数据的一致性,Zookeeper采用了ZAB协议,这种协议非常类似于一致性算法Paxos和Raft.

什么是ZAB协议
Zookeeper Atomic Broadcast 可以解决两个问题
  • 集群崩溃恢复
  • 主从同步数据
ZAB协议定义的三种节点状态
  • Looking :选举状态
  • Following:Follower节点所处的状态
  • Leading:Leader节点所处的状态
最大ZXID

最大ZXID也就是节点本地最新事务编号,可以理解为自增id

每个节点都有一个ZXID,谁的ZXID大,谁的数据就是最新的

ZAB的崩溃恢复

当Zookeeper的主节点崩溃了,集群会进行崩溃恢复,ZAB的崩溃恢复会进行三个阶段:

  1. Leader election

    选举阶段,此时集群的节点处于Looking阶段,它们会各自向其他节点发起投票,投票当中包含自己的服务器ID和最新的事务ID(ZXID)

    接下来,节点就会用自身的ZXID和从其他节点接收到的ZXID做比较,如果发现别人家的ZXID比自己的大,也就是数据比自己的新,就重新发起投票,投票给目前已知的最大的ZXID所有节点

    每次投票,服务器都会统计投票数量,判断是否有节点得到半数以上的投票,如果有这样的节点,该节点将会成为准Leader节点,就是Leading,其他节点的状态为Following

imgimgimg
  1. Discover

    发现阶段,用于从节点中发现最新的ZXID和事务日志

    问题:既然第一个阶段,Leader被选为主节点,已经是集群中数据最新的了, 为什么还要从集群中寻找最新事务呢?

    答案:为了防止因为网络问题,在上一阶段出现多个Leader的情况

    在这个阶段,Leader接收所有Following发来的各自的epoch值,Leader从中选出最大的epoch值,加1,返回给所有的Following

    各个Following收到全新的epoch后,返回ACK给Leader,带上各自的ZXID和最新事务日志,Leader选出最大的ZXID,并更新事务日志

  2. Synchronization

    同步阶段,把Leader接收到的最新事务日志,同步给集群中所有的Following,只有当半数Following同步成功,这个准Leader才会成为Leader

ZAB的数据恢复

​ Broadcast:广播

常规情况下更新数据的时候,由Leader广播到所有的Following

  1. 客户端发出写入请求给任意的Following
  2. Following把写入请求转发给Leader节点
  3. Leader采用二阶段提交的方法,先发送Propose广播给Following
  4. Following接收到Propose,写入日志成功后,返回ACK消息给Leader
  5. Leader接收到半数以上的ACK,返回成功给客户端,并且广播Commit请求给Following
img

ZAB协议即不是强一致性,也不是弱一致性,而是顺序一致性,它依靠事务ID和版本号,保证了数据的更新和读取是有序的

Zookeeper分布式锁

什么是临时顺序节点

Znode分为四种类型:

持久节点,持久节点顺序节点

临时节点,临时顺序节点

临时节点:当创建节点的客户端与Zookeeper断开连接后,临时节点会被删除

临时顺序节点:在创建节点时,Zookeeper根据创建的时候顺序给该节点名称进行编号;当创建节点的客户端与Zookeeper断开连接后,临时节点会被删除

Zookeeper分布式锁原理

Zookeeper分布式锁应用了临时顺序节点,来实现,看下详细步骤:

  1. 获取锁

    首先,在Zookeeper当中创建一个持久节点ParentLock,当第一个客户端想要获得锁时,就需要在ParentLock这个节点下面创建一个临时顺序节点Lock1

    img

​ 之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个,如果是第一个节点,则成功获得锁

img

这时候,如果再有一个客户端Client2来获取锁,则在ParentLock再创建一个顺序临时节点Lock2

img

Client2查找ParentLock下面所有顺序临时节点并排序,判断自己创建的Lock2是不是排名最靠前的一个,结果发现Lock2并不是最靠前的

于是,Client2就排序向仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在,意味这此时Lock2抢锁失败,进入到等待状态

img

这时候,如果又有一个客户端Client3前来获取锁,则在ParentLock下再创建一个临时顺序节点Lock3

img

Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己创建的Lock3是否是排名最靠前的,发现不是,Client3就向排序仅比它靠前的Lock2注册Watcher,用于监听Lock2是否存在,Client3也抢锁失败,进入到了等待状态

img

这样,Client1得到锁,Client2和Client3等待状态

  1. 释放锁

    释放锁分为两种

    • 任务完成,客户端释放锁

      当任务完成时,Client1会显示调用删除节点Lock1的指令

      img

    • 任务执行过程中,客户端崩溃

      获得锁的Client1,如果崩溃,与Zookeeper服务端的连接断开,根据临时顺序节点的特性,相关联的Lock1会自动删除

      img

    此时由于Client2一直监听这Lock1的存在状态,当Lock1被删除,Client2会立即收到通知,这个时候,Client2会再次查询ParentLock下面的所有节点,确认自己的Lock2节点是否是排名最靠前的,如果是,则获取到锁

Zookeeper和Redis分布式锁的比较

img

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值