写在前面

本文隶属于专栏《100个问题搞定大数据理论体系》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!

本专栏目录结构和文献引用请见100个问题搞定大数据理论体系

解答

在分布式环境下,为了保证数据的一致性,需要利用分布式锁技术来保证同一时刻只有固定数量的进程对数据进行修改:只有获取锁的客户端才能对数据进行修改,其余客户端只能暂时等待。

实现方式:
1.数据库的唯一索引
2.Redis的SETNX指令
3.Redis的RedLock算法
4.ZooKeeper的临时顺序节点

补充

阻塞锁通常使用互斥量来实现:
互斥量为0表示有其它进程在使用锁,此时处于锁定状态互斥量为1表示未锁定状态。
1和0可以用一个整型值表示,也可以用某个数据是否存在表示。

数据库的唯一索引

获得锁时向表中插入一条记录,释放锁时删除这条记录。
唯一索引可以保证该记录只被插入一次,那么就可以用这个记录是否存在来判断是否处于锁定状态。

存在以下几个问题

  1. 锁没有失效时间,解锁失败的话其它进程无法再获得该锁;
  2. 只能是非阻塞锁,插入失败直接就报错了,无法重试;
  3. 不可重入,已经获得锁的进程也必须重新获取锁。

Redis的SETNX指令

  1. 使用SETNX(set if not exist)指令插入一个键值对,如果Key已经存在,那么会返回False,否则插入成功并返回True。
  2. SETNX指令和数据库的唯一索引类似,保证了只存在一个Key的键值对,那么可以用一个Key的键值对是否存在来判断是否存于锁定状态。
  3. EXPIRE指令可以为一个键值对设置一个过期时间,从而避免了数据库唯一索引实现方式中释放锁失败的问题。

Redis的RedLock算法

  1. 使用了多个 Redis实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用尝试从N个互相独立 Redis实例获取锁;
  2. 计算获取锁消耗的时间,只有时间小于锁的过期时间,并且从大多数(N/2+1)实例上获取了锁,才认为获取锁成功
  3. 如果获取锁失败,就到每个实例上释放锁。

ZooKeeper的临时顺序节点

概念

Zookeeper中临时顺序节点的生命周期和客户端会话是绑定的,即:
创建节点的客户端会话一且失效,那么这个节点也会被清除。
而且每个临时顺序节点的父节点都会负责记录其子节点创建的先后顺序,并自动为这个子节点分配一个整型数值,以后缀的形式自动追加到节点名中,作为这个节点最终的节点名。

实现流程

Zookeeper的分布式锁机制正是利用临时顺序节点的上述特性实现的,其基本流程如下

  1. 客户端调用create()方法创建父节点_locknode_与其子节点_locknode_/guid-lock-,注意所创建节点的类型需要设置为EPHEMERAL_SEQUENTIAL。
  2. 客户端调用 getchildren("locknode")方法来获取所有已经创建的子节点,同时在这些子节点上注册 Watcher。
  3. 客户端获取了所有子节点之后,如果发现自己在步骤1中创建的子节点是所有子节点中序号最小的,就说明自己已经获取到了锁。
  4. 如果客户端在步骤3中发现自己创建的子节点并非是所有子节点中序号最小的, 说明自己还没有获取到锁,需要等待,直至接到 Watcher发送的子节点变更通知(即其他客户端释放锁)之后,オ能再获取一次子节点,以判断自己是否获取到了锁。

释放锁的过程相对比较简单,删除客户端自己创建的子节点即可。

ZK 实现分布式锁

应用示例

以下是一个 Zookeeper分布式锁的应用示例:

  1. 客户端A与客户端B都希望获取分布式锁,为此,它们首先要在_locknode_节点下创建一个临时顺序节点guid-lock-n(n为Zookeeper自动分配的整数),然后立即获取acnode下的所有(一级)子节点。
  2. 由于A与B两个客户端在同一时间争取锁,因此_locknode_下的子节点数量会大于1。而顺序节点的特点是节点名称后会自动有一个数字编号,先创建的节点数字编号小于后创建的,因此可将子节点按照节点名称后的数字编号从小到大排序,排在第一位的(即数字编号最小的)就是最先创建的顺序节点,该节点的创建者就是争取到锁的客户端。
  3. 接下来,客户端A需要判断最小的这个节点是否是自己创建的。如果是,则表示客户端A获取到了锁;如果不是,则表示锁已经被其他客户端获取,客户端A就要等待该客户端释放锁,也就是等待获取到锁的客户端B把自己创建的节点删除。
  4. 客户端A可以通过监听比自己创建的节点guid-lock-n次小的那个顺序节点的删除事件来推测客户端B是否已经释放了锁。如果是,此时客户端A可再次获取_locknode_下的所有子节点,将其再次与自己创建的guid-lock-n节点对比,直到确定自己创建的guid-lock-n已成为_locknode_下的所有子节点中顺序号最小的,就表示自己已获取到了锁。

Q.E.D.


Apache Spark Contributor