zookeeper分布式锁实现原理

1、互斥锁mutex lock

顾名思义就是排它锁,同一时间只允许一个客户端执行。

实现步骤:

  • 首先,创建一个lock node,例如“locknode”
  • 其次,客户端lock执行以下方式: 
  1. 创建(create)一个有序临时节点,例如“locknode/guid-lock-”,其中guid可以是你客户端的唯一识别序号,如果发生前面说的创建失败问题,需要使用guid进行手动检查。
  2. 调用getChildren(watch=false)获取获取子节点列表,注意wtach设置为false,以避免羊群效应(Herd Effect),即同时收到太多无效节点删除通知。
  3. 从这个列表中,判断自己创建的节点序号是否是最小,如果是则直接返回true,否则继续往下走。
  4. 从步骤2中获取的list中选取排在当前节点前一位的节点,调用exist(watch=true)方法。
  5. 如果exist返回false,则回到步骤2;
  6. 如果exist返回true,则等待exist的哨兵(watch)回调通知,收到通知后再执行步骤2.
  • 最后,客户端unlock只需要调用delete删除掉节点即可。

节点操作示意图:

流程图:

优点:

  • 避免了轮询和超时控制
  • 每次一个子节点的删除动作,只会触发唯一一个客户端的watch动作,从而避免了羊群效应
  • 便于观测

缺点:

  • 没有解决锁重入问题,因为采用的是有序临时节点,因此多次调用create并不会触发KeeperException.NodeExists异常,从而无法实现锁重入功能。如果需要解决,则在步骤1时,需要先进行判断当前节点是否已经存在,即调用getChildren(watch=false),判断当前节点是否已经创建(配合guid),已经创建,则直接从步骤3开始,没有创建则从步骤1开始。
  • 这是一个公平锁,无法实现非公平锁。参考[4]实现了一个非公平锁

注意:

如果一个节点创建了一个sequential ephemeral nodes,但是在server返回创建成功的时候,server挂了,此时客户端需要重新连接,重新连接后会话依然有效,但其创建的临时节点却没有删除。解决方式就是在每次创建时create,如果发生失败,客户端需要getChildren(),进行手动检查是否获取锁,这个时候就需要使用guid。

2、共享锁Shared Locks或读写锁Read/Write Locks

Read读锁是共享锁,Write写锁是排它锁,当没有写时,允许多个read实例获取读锁,当有一个write实例获得写锁时,则不允许任何其他write实例和read实例获得锁。

实现步骤:

  • 首先,创建一个lock node,例如“locknode”
  • 获取read锁步骤: 
  1. 创建(create)一个有序临时节点,例如“locknode/read-guid-lock-”,其中guid可以是你客户端的唯一识别序号,如果发生前面说的创建失败问题,需要使用guid进行手动检查。
  2. 调用getChildren(watch=false)获取获取子节点列表,注意wtach设置为false,以避免羊群效应(Herd Effect),即同时收到太多无效节点删除通知。
  3. 从这个列表中,判断是否有序号比自己小、且路径名以“write-”开头的节点,如果没有,则直接获取读锁,否则继续如下步骤。
  4. 从步骤2中获取的list中选取排在当前节点前一位的、且路径名以“write-”开头的节点,调用exist(watch=true)方法。
  5. 如果exist返回false,则回到步骤2。
  6. 如果exist返回true,则等待exist的哨兵(watch)回调通知,收到通知后再执行步骤2。
  • 获取write锁步骤: 
  1. 创建(create)一个有序临时节点,例如“locknode/write-guid-lock-”,其中guid可以是你客户端的唯一识别序号,如果发生前面说的创建失败问题,需要使用guid进行手动检查。
  2. 调用getChildren(watch=false)获取获取子节点列表,注意wtach设置为false,以避免羊群效应(Herd Effect),即同时收到太多无效节点删除通知。
  3. 从这个列表中,判断自己创建的节点序号是否是最小,如果是则直接返回true,否则继续往下走。
  4. 从步骤2中获取的list中选取排在当前节点前一位的节点,调用exist(watch=true)方法。
  5. 如果exist返回false,则回到步骤2;
  6. 如果exist返回true,则等待exist的哨兵(watch)回调通知,收到通知后再执行步骤2.
  • 最后,客户端unlock只需要调用delete删除掉节点即可。
     

节点操作示意图:

流程图:

  • read lock

  • write lock

优点:

  • 避免了轮询和超时控制
  • 每次一个子节点的删除动作,只会触发唯一一个客户端的watch动作,从而避免了羊群效应
  • 便于观测

缺点:

  • 没有解决锁重入问题,因为采用的是有序临时节点,因此多次调用create并不会触发KeeperException.NodeExists异常,从而无法实现锁重入功能。如果需要解决,则在步骤1时,需要先进行判断当前节点是否已经存在,即调用getChildren(watch=false),判断当前节点是否已经创建(配合guid),已经创建,则直接从步骤3开始,没有创建则从步骤1开始。
  • 当有非常多的read节点在等待一个write节点删除通知时,一旦write节点删除,将会触发非常多的read节点被调用,不过这种情况无法避免。

可撤销和超时问题

当前的读写锁并没有考虑读锁可撤销和超时问题,如何让读锁主动放弃,如何判断超时等,我想可行的方案还是在客户端自己处理,如果其他客户端想让前面的节点放弃锁,可以在节点写入unlock信息,让持有锁的客户端监听该变化,收到unlock信息,自己主动放弃对锁的持有。

3、参考

[1] http://zookeeper.apache.org/doc/trunk/recipes.html#sc_recipes_Locks 
[2] http://blog.csdn.net/xubo_zhang/article/details/8506163 
[3] http://netcome.iteye.com/blog/1474255 一个比较好的介绍 
[4] http://blog.csdn.net/nimasike/article/details/51567653#reply 实现了一种非公平锁
 

你可能感兴趣的:(JAVA)