Zookeeper 实现分布式锁

·  阅读 419

分布式锁适用场景

以商品秒杀下的减库存场景为例,假设我们某个商品的库存为 1,并存储在 redis 中,我们的程序是先读 redis 确认大于 0 时,在将 redis 中的库存减 1,最终目标是保证商品不超卖!

如果我们的服务只有一个实例,在单台机器上可以通过 Synchronized 等锁机制,控制单机并发,保证同一时间只有一个线程能执行上述操作

但如果服务有多个实例,那么 Synchronized 等锁机制是无法控制其他实例服务不对共享资源进行访问的,这个时候就需要使用分布式锁来解决

Znode 的节点类型

  • 临时节点 :当创建临时节点的客户端与 zookeeper 连接(会话)断开后,临时节点会被删除

  • 持久节点 :默认的节点类型。创建节点的客户端与 zookeeper 断开连接后,该节点不会被删除

  • 顺序节点 : 顺序节点 会在节点名称后面追加一个由父节点维护的自增整型数字,这个数字相对于父节点是唯一的,格式为由0填充的10位数字,如 “nodeName0000000001”

    • 临时顺序节点 :在临时节点的基础上增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字
    • 持久顺序节点 :在持久节点的基础上增加了顺序属性,节点名后边会追加一个由父节点维护的自增整型数字

持久节点

创建持久节点

create /nodeName 节点内容
复制代码

持久顺序节点

创建持久顺序节点

create -s /nodeName 节点内容 
复制代码

临时节点

创建临时节点

create -e /nodeName 节点内容 
复制代码

当创建临时节点的客户端与 zookeeper 连接(会话)断开后,临时节点会被删除

临时顺序节点

创建临时顺序节点

create -e -s /nodeName 节点内容 
复制代码

利用持久节点实现分布式锁

监听器 Watches

Zookeeper 客户端可以再使用下面几个方法的时候,给 znode 添加一个监听器

  • getData() : 获取某个节点的数据
  • getChildren() : 获取某个节点的所有子节点
  • exists() : 查看某个节点是否存在

当发生 Created event、Deleted event 等事件的时候,Zookeeper 就会通知客户端,客户端就会调用预先绑定事件的回调方法

持久节点实现分布式锁

我们利用持久节点和监听机制就可以实现分布式锁,以文章开的控制商品超卖为例,我们可以这样设计

  1. 所有的服务在减库存前,都要获取到分布式式锁才能继续操作,这个分布式锁就是一个 Zookeeper 的持久节点,比如叫做 iphone_inventory_lock (iphone库存锁)
  2. 所有的服务在减库存前,先调用 Zookeeper 客户端的 exists() 方法,查看 iphone_inventory_lock 节点是否存在?
  3. 如果存在就,监听该节点的删除事件
  4. 如果不存在,就创建该节点
  5. 如果创建成功,则相对于拥有了锁,可以去操作共享资源
  6. 如果创建失败,说明有其他客户端创建成功了,就去监听节点的删除事件
  7. 操作共享资源后,调用 Zookeeper 客户端的 delete() 方法手动删除 iphone_inventory_lock 节点
  8. iphone_inventory_lock 节点被删除后,会触发 delete 事件,Zookeeper 会通知所有监听了该节点 delete 事件的客户端
  9. 收到 delete 事件的客户端会再次尝试创建 iphone_inventory_lock 节点

只要所有操作共享资源的服务都遵循以上流程,就实现了分布式锁

但使用持久节点来实现分布式锁有一个很严重的问题

如果创建 iphone_inventory_lock 节点的服务,在业务处理过程中宕机了,没来得及手动删除 iphone_inventory_lock 节点,则会导致一直“持有”锁,其他服务将会一直等待下去

为了解决这个问题,我们需要使用临时节点来实现分布式锁

临时节点实现分布式锁

使用临时节点来实现分布式锁,跟使用持久节点实现分布式锁流程完全一样,唯一的区别就是 iphone_inventory_lock 节点不在是持久节点而是临时节点

好处在于,假如在业务处理过程中宕机了,没来得及手动删除 iphone_inventory_lock 节点时,由于宕机后无法维持与 Zookeeper 的连接(心跳)Zookeeper 会在连接(心跳)断开后,自动删除 iphone_inventory_lock 节点,这样就不然导致死锁

其实,上面两种方式还有一个很大的问题,假设现在竞争很激烈,有 1000 个监听器都在监听同一个节点,这个节点被删除时,Zookeeper 需要通知 1000 个监听器,这 1000 个监听器收到通知后,都去并发创建节点,结果只有一个能创建成功拿到锁,造成非常大的性能浪费。

由于上面的场景非常像,牧羊犬赶羊出圈,羊群涌往出口,所有也被称为羊群效应。反正我觉得不像,我觉得羊群效应这个名词更像是拿来唬人的噱头

怎么解决 羊群效应 呢,我们可以使用持久节点 + 临时顺序节点 来实现分布式锁

持久节点 + 临时顺序节点实现分布式锁

以文章开的控制商品超卖为例,我们先创建一个代表某一种锁的持久节点,比如叫 iphone_inventory_lock,然后

  1. 所有的服务在减库存前,先在 iphone_inventory_lock 这个永久节点下创建一个临时顺序节点,由于临时顺序节点尾部序号是自增且不重复的,所以都能创建成功
  2. 调用 Zookeeper 客户端的 getChildren() 方法,获取 iphone_inventory_lock 节点下面的的所有子节点
  3. 查看自己是不是第一个节点
  4. 如果是,操作共享资源,之后手动删除自己的节点
  5. 如果不是,找到自己临时顺序节点的前一个节点,注册监听器,监听删除事件。比如自己的临时顺序节点为 iphone_inventory_lock0000000002,那么就监听上一个节点 iphone_inventory_lock0000000001
  6. 某个节点被删除后,假设被删除的节点为 iphone_inventory_lock0000000001, 由于下一个节点 iphone_inventory_lock0000000002 监听了 iphone_inventory_lock0000000001 的删除事件,收到事件后它会回到第 2 步

每个节点监听前一个节点,按照先来后到的顺序获取锁,就像一个先进先出的队列,同时这种方式实现了公平锁

Apache Curator

知道了原理后,具体怎么实现呢?其实我们没有必要手动实现,因为已经有了开源的实现方案 - Apache Curator

Apache Curator 是一个开源的 Zookeeper 客户端 ,提供了对 Zookeeper 分布式锁的实现。

Apache Curator 锁

Shared Lock (共享锁) : 全局同步的分布式锁,多个客户端可以共享同一把锁,但不支持重入

Shared Reentrant Lock (共享可重入锁) : 跟 Shared Lock 一样但支持重入

Shared Reentrant Read Write Lock (共享可重入读写锁) : 可重入的读 / 写互斥、读 / 读共享分布式锁。此外,这里的读 / 写互斥是 “公平的”,每个用户都会按照请求的顺序(从 ZK 的角度来看)获得读 / 写互斥锁。 一个 Shared Reentrant Read Write Lock 维护着一对关联的锁,一个用于只读操作,一个用于写操作。发生写操作时(获取到写锁时),读锁无法被获取,但只要没有写操作,读锁就可以被多个读进程同时持有(写锁只能被一个进程持有),同时它还支持重入。此外,写线程可以获取读锁。

Shared Semaphore (共享信号量) : 分布式的计数信号量。使用相同锁路径的所有 JVM 中的所有进程将实现进程间有限的租用集。此外,这个信号量是 “公平的”—— 每个用户将按照请求的顺序获得(从 ZK 的角度来看)。

和redis分布式锁的对比

关于 redis 分布式锁,参考 Redis 实现分布式锁

  • 维护成本

    • redis 单节点分布式锁 : 需要维护一个单独的 redis 服务

    • Redlock : 需要维护至少 5个 独立的 redis 服务

    • zookeeper 分布式锁 : zookeeper 集群本身

  • 申请锁性能(并发)

    • redis 单节点分布式锁 : 申请锁只是执行 SETNX 命令,性能很高

    • Redlock : 需要向半数以上节点申请锁,性能不如 redis 单节点分布式锁

    • zookeeper 分布式锁 : 由于 zookeeperCP 模式,追求强一致性,添加 znode时,都要同步到集群中的其他节点,所以效率上不如 redis 分布式锁

  • 释放锁性能

    • redis 单节点分布式锁 : 释放锁只是执行 Lua 脚本,性能很高
    • Redlock : 需要向所有节点释放锁,性能不如 redis 单节点分布式锁
    • zookeeper 分布式锁 : 由于 zookeeperCP 模式,追求强一致性,添加、删除 znode时,都要同步到集群中的其他节点,所以效率上不如 redis 分布式锁

redis 单节点分布式锁zookeeper 分布式锁 性能对比

  Zookeeper << Redis

  技术分享

  • 可靠性(一致性)

    • redis 单节点分布式锁 : 需要依赖守护线程自动续期、为锁设置过期时间、lua 脚本,可靠性(一致性)一般

    • Redlock : 除了以上条件,还需要依赖多个 redis 节点、时钟正确等、可靠性不如redis 单节点分布式锁

    • zookeeper 分布式锁 : 基于心跳机制,不需要考虑锁的过期时间,可靠性(一致性)非常高

  • 使用成本(cpu资源消耗)

    • redis 单节点分布式锁 : 需要不断轮询获取锁

    • Redlock : 需要不断轮询获取锁

    • zookeeper 分布式锁 : 基于 watch 机制,不需要轮询

  • 使用复杂度

    • redis 单节点分布式锁 : 有第三方实现——Redisson

    • Redlock : 有第三方实现——Redisson

    • zookeeper 分布式锁 : 有第三方实现——Curator

redis 单节点分布式锁Redlockzookeeper分布式锁
维护成本
申请锁性能(并发)
释放锁性能
可靠性(强一致性)
实现复杂度
使用成本(cpu资源消耗)
使用复杂度Redisson(低)Redisson(低)Curator(低)

分布式锁的选择

Redlock 实现复杂且条件苛刻,性能和可靠行也不如redis 单节点分布式锁,如果要用 redis 实现分布式锁建议还是选择单节点这种模式,虽然可靠性上不如 Zookeeper 的方式,但胜在性能好

但如果你的项目追求高可靠、强一致则建议选择 Zookeeper 实现分布式锁 这种方式

关键在于你是追求性能,还是追求高可靠、强一致

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改