ZooKeeper分布式锁的实现

一、前言

  • 在介绍分布式锁之前,我们来聊一聊锁的种类

线程锁

  • 线程锁就是在同一个进程中访问临界资源时使用的锁,主要是用来线程间同步与互斥的
  • 以Linux为例,常用的线程锁有:互斥量、读写锁、条件变量、自旋锁等...

进程锁

  • 例如Nginx里面有一个accept锁,是使用共享内存+信号量构成的

分布式锁

  • 不同机器的不同进程之间的锁

二、分布式锁的实现方式

  • 常见的实现方式有:
    • Redis分布式锁
    • MySQL分布式锁
    • ZooKeeper分布式锁(最常用)
    • ......其他还有,可以自行百度搜索
  • 高效性:ZooKeeper > Redis > MySQL

三、分布式锁的特性

互斥性

  • 锁的最基本特征,保证临界资源只能被一者访问

可重入性

  • 一个进程允许递归获取锁(并且可以递归释放锁)

锁超时

  • 当客户端或者服务中心crash掉之后,crash掉之后,锁需要被释放掉

高效性、可用性

  • 数据中心需要集群,ZooKeeper、Redis等都提供了集群的功能,集群保证数据中心一台机器crash掉之后,其他机器仍然能正常工作,保证机器在crash掉之后能够正确的重新获取锁和释放锁
  • 对于高可用性来说:ZooKeeper是强于Redis的
    • 因为Redis保证的是最终一致性:对于主从服务器来说,当从服务器crash掉之后,会导致至少有1秒(rdb方式运行)或者至少有1个事务操作(aof方式运行)丢失,因为此时可能会有新的数据写入了主服务器,但是从服务器没有更新。但是当从服务器再次重启之后会根据复制偏移量来与主服务器进行数据同步,达到数据最终一致
    • ZooKeeper保证的是强一致性:ZooKeeper通过zab协议使得每个节点之间的数据是一直保持一致的

公平锁、非公平锁

  • 公平锁:各个进程按照顺序获取锁
  • 非公平锁:各个进程获取锁的顺序可以是不按照顺序执行的
  • 还有一系列其他特性等

四、ZooKeeper语法

  • 在介绍ZooKeeper分布式锁之前,介绍一下ZooKeeper的两个语法,ZooKeeper分布式锁主要就是用到这两个语法

ZooKeeper节点

  • ZooKeeper节点有4种类型:
    • 持久性节点:节点创建之后会一直存在ZooKeeper当中(持久化到硬盘中了),因此即使ZooKeeper服务重启也依然存在,只有当手动调用delete删除节点之后,节点才被删除
    • 临时性节点:由客户端创建的,例如由客户端A创建,只要客户端A没有断开ZooKeeper服务,那么这个节点就存在ZooKeeper中(其他客户端也可以看到客户端A创建的这个临时性节点),当客户端A退出之后该临时性节点会被自动删除,或者主动调用delete删除这个节点
    • 持久顺序性节点:节点是持久性的,并且是有序的,有序节点从0000000000开始递增,之后创建的节点为0000000001......一次类推
    • 临时顺序性节点:节点是临时性的,并且是有序的,有序节点从0000000000开始递增,之后创建的节点为0000000001......一次类推
  • 语法:
# 不加任何参数时, 创建的是"持久性"节点
create /persistent_node

# -e参数用来创建"临时性"节点
create -e /temp_node

# -s参数用来创建"持久有序性"节点
create -s /persistent_orderly_node

# -s -e的组合用来创建"临时有序性"节点
create -s -e /temp_orderly_node

ZooKeeper监听机制

  • ZooKeeper节点可以调用addWatch命令对一个节点进行监听,当节点被创建、删除、修改的时候会获取相应的通知

五、ZooKeeper分布式锁的实现原理

  • ZooKeeper分布式锁是通过"数据模型+监听机制"实现的

六、方案1:使用临时性节点实现(但是存在惊群现象)

实现思路

  • 使用ZooKeeper的临时节点作为锁
  • 大致过程:
    • 第一步(加锁):使用指定的临时节点作为锁
      • 如果临时节点不存在,那么创建临时节点(加锁),其他的进程想要创建临时节点(加锁)就阻塞等待
      • 如果临时节点已存在,那么说明其他进程已经加锁了,就调用addWatch命令监听临时节点被删除(解锁)
    • 第二步(操作临界资源):第一步获取了锁之后,对临界资源进行操作
    • 第三步(释放锁):操作完成之后,调用delete删除临时节点(释放锁),那么其他监视到临时节点被删除(解锁),那么就可以重新获取锁了
  • 例如:

ZooKeeper分布式锁的实现_第1张图片

不要使用持久性节点,要使用临时性节点

  • 如果使用持久性节点:当一个进程创建了持久性节点之后(加锁了),如果其进程阻塞了,那么这个持久性节点就永远不会被删除了,其他进程永远都不会获取锁了,因此就造成了死锁现象
  • 如果使用临时性节点:因为ZooKeeper服务端与客户端之间有心跳检测机制(配置文件中的tickTime选项),如果一个进程阻塞了,那么服务端会通过心跳检测到该客户端超时了,那么就会自动这条客户端连接,当客户端连接断开之后,临时性节点会自动被删除,其他节点就可以获取锁了,因此使用临时性节点不会造成死锁问题

该方案存在的问题

  • 如果ZooKeeper的客户端很多的话,如果一个客户端释放锁了,其他的客户端都会被唤醒来抢占锁,从而产生了惊群现象,会造成网络资源的消耗

七、方案2:使用临时顺序性节点实现(有错误性获取锁的问题)

实现思路

  • 我们知道顺序性节点创建的时候会自动从0000000000、0000000001、0000000002、......向后依次类推
  • 该方案的实现思路就是:让进程监听在比自己小的临时顺序性节点上,只有当监听的节点被释放之后,自己才创建属于自己的临时顺序性节点(加锁),后面的节点依次类推
  • 这种方式其实就是公平锁的规则,让所有的进程有顺序的去获取锁

ZooKeeper分布式锁的实现_第2张图片

该方式存在一个问题

  • 假设A获取锁了,B监听A,C监听B,如果此时B crash了,那么C发现B的节点消失了,那么C就去获取锁了,但是此时A没有执行完,出现了2次加锁的现象

ZooKeeper分布式锁的实现_第3张图片

八、最终解决方案:自动更新监听节点

实现思路

  • 上面的问题主要就是如果其监听的节点crash了,那么就会错误性地获取锁
  • 对于这种问题,我们的解决思路是:如果一个节点其监听的节点crash了,那么就将其更改为前一节点监听的节点
  • 例如:对于上图来说,B监听A,C监听B,如果B crash了,那么就让C去监听A,这样的话就不会产生错误了

你可能感兴趣的:(架构师进阶,ZooKeeper分布式锁)