logo

分布式解决方案 -- Zookeeper 分布式锁原理实现

发布时间: April 5, 2021

Zookeeper 是一种分布式应用程序协调服务,用于在分布式环境中提供一种简单的架构和 API 来解决多服务协调和管理的问题。Zookeeper 使开发人员不必关系分布式应用中的一些底层细节,从而专注于应用程序的核心逻辑。

所谓「分布式协调服务」,就是用来解决分布式环境当中多个进程之间的同步控制,让他们有序的去访问某种临界资源,防止造成”脏数据”的后果。 为了保证分布式系统多进程能够有序的访问该临界资源,就需要用到一种分布式协调技术,而分布式锁就是分布式协调技术实现的核心内容。

分布式锁应该具备哪些条件?

  • 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
  • 高可用的获取锁与释放锁
  • 高性能的获取锁与释放锁
  • 具备可重入特性 ( 可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误 )
  • 具备锁失效机制,防止死锁
  • 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败

分布式锁的实现有哪些?

说到分布式锁,最容易想起的就是 Redis 实现分布式锁。但是 Redis 并不是为了实现分布式锁而设计出来的,他只是一个 NoSql 内存数据库。我们可以利用 Redis 中一些 API 特性实现分布式锁。 不过用 Redis 来实现分布式锁会遇到的种种问题,在网上也有大佬辩论过了,提供两篇文章可以参考下: 基于 Redis 的分布式锁到底安全吗 ( 上 ) ? 基于 Redis 的分布式锁到底安全吗 ( 下 ) ?

另外就还有 MySQL 和 Google 公司的 Chubby 可以实现分布式锁

Zookeeper 的一些基本概念

数据模型

Zookeeper 的数据模型很像数据结构中的树,也很像文件系统中的目录。Zookeeper 的数据存储基于节点,这种节点叫做 Znode. Znode 的引用方式是路径引用,类似于文件路径:

/server1/u1
/server2/k1

这样的层级结构,让每一个 Znode 节点有唯一的路径,像命名空间一样,对不同信息作出隔离。

Znode 包含的元素

  • data: Znode 存储的数据信息。
  • ACL:记录 Znode 的访问权限,即哪些人或哪些 IP 可以访问本节点。
  • stat:包含 Znode 的各种元数据,比如事务 ID、版本号、时间戳、大小等等。
  • child:当前节点的子节点引用

Znode 类型

  • 持久节点: 默认的节点类型。创建节点的客户端与 Zookeeper 断开连接后,该节点依旧存在。
  • 持久节点顺序节点:所谓顺序节点,就是在创建节点时,Zookeeper 根据创建的时间顺序给该节点名称进行编号。
  • 临时节点:和持久节点相反,当创建节点的客户端与 Zookeeper 断开连接后,临时节点会被删除。
  • 临时顺序节点:临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper 根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与 Zookeeper 断开连接后,临时节点会被删除。

事件通知机制

Zookeeper 的基本操作有 createdeleteexistsgetDatasetDatagetChildren 等,而对于属于读的操作,客户端在请求的时候可以选择是否设置 Watch,也就是 Zookeeper 的事件通知。 可以把 Watch 理解成是注册在特定 Znode 上的触发器。当这个 Znode 发生改变,也就是调用了 create, delete, setData 方法的时候,将会触发 Znode 上注册的对应事件,请求 Watch 的客户端会接收到异步通知。 具体交互如下:

  • 客户端调用 getData 方法,watch 参数是 true。服务端接到请求,返回节点数据,并且在对应的哈希表里插入被 Watch 的 Znode 路径,以及 Watcher 列表。
  • 当被 Watch 的 Znode 已删除,服务端会查找哈希表,找到该 Znode 对应的所有 Watcher,异步通知客户端,并且删除哈希表中对应的 Key-Value。

Zookeeper 实现分布式锁原理

通过 Zookeeper 的基本概念可以看出,可以用 临时顺序节点Watch 机制实现分布式锁。 加锁与解锁:

  1. 首先在 Zookeeper 中创建一个持久节点 RootLock ( 即为服务创建临时顺序节点的根节点 ) 。
  2. 当 Client1 需要获取锁时,需要在 RootLock 下创建 临时顺序节点 Lock1。然后查找 RootLock 下所有临时顺序节点,并排序,判断自己创建的节点 Lock1 是否在顺序最靠前的一个,如果是第一个,则获取锁成功。
  3. 这时 Client2 来获取锁时,先在 RootLock 下创建一个临时顺序节点 Lock2。然后 Client2 查找 RootLock 下所有临时顺序节点,并排序,判断自己创建的节点 Lock2 是否在顺序的第一个,发现 Lock2 并不是最小的,获取锁失败。于是 Client2 向排序比它 Lock2 节点靠前一位的 Lock1 节点注册 Watch,用于监听 Lock1 节点是否存在,意味着进入等待状态。
  4. 后续如果 Client3 来获取锁,执行一样的操作,创建 Lock3 之后查找排序,判断是否第一个,如果不是就监听比他高一位的 Lock2 节点,进入等待。
  5. 当任务完成时,Client1 会显示调用删除节点 Lock1 的指令。而当任务执行过程中 Client1 崩溃,则会断开 Zookeeper 服务端链接。根据临时节点的特性,关联节点 Lock1 则会自动删除。
  6. 由于 Client2 监听着 Lock1 的存在状态,当 Lock1 被删除时,Client2 会立即收到通知。这时 Client2 会再次查询 RootLock 下的所有节点并排序,确认自己创建的节点 Lock2 是否是第一个节点。如果是,则 Client2 获得锁。
  7. 同理 Client2 任务完成或者崩溃而删除了 Lock2,那么 Client3 就会收到通知获得锁。

总结

通过实现分布式锁的原理可以看出,由于 Zookeeper 天生设计定位就是分布式协调,强一致性。锁的模型健壮、简单易用、适合做分布式锁。用来实现分布式锁非常方便,而且不用去考虑超时,过期等问题,同时还有等待锁的队列,也大大提高了抢锁效率。

那么用 Zookeeper 实现分布式锁有没有一些问题呢?如果有较多的客户端频繁的申请加锁、释放锁,对于 Zookeeper 集群的压力会比较大,性能相比使用 Redis 低。

至于如何选用分布式锁,就看具体公司的场景了,个人比较推崇 Zookeeper 实现锁。