这就是搜索引擎(5) 云存储之分布式锁服务

1. 背景

在分布式系统中,不同机器的进程之间往往需要协调动作,如果不同的进程之间共享了同一个或者同一组资源,就需要对他们的行为进行互斥来保证一致性,这个时候就需要使用到分布式锁。
分布式锁,是控制分布式系统之间同步访问共享资源的一种方式。在传统的单体应用中,我们也会使用锁来解决线程之间共享资源访问的问题,分布式锁与传统锁不同的一个点在于,分布式锁将共享资源的最小粒度从线程升级到了进程。

2. 分布式锁的特点

一个分布式锁需要具备哪些条件?

  1. 互斥性:在分布式系统的环境下,分布式锁同一时间只能被同一个机器的一个线程获取
  2. 高可用、高性能的获取和释放锁
  3. 具备锁失效机制,即通过超时等方式自动解锁来防止死锁
  4. 具备锁的可重入机制,即持有锁的线程可以再次获取锁

3. 分布式锁的三种实现方式

3.1 基于数据库实现

基于数据库的实现主要是利用数据库的唯一索引来实现,因为唯一索引具备排他性,因此我们在加锁时插入一条记录,记录的唯一索引值为业务id(保证不同业务之间互不影响),在解锁时再对这条记录进行删除,期间如果有其他的竞争者进行加锁,就会抛出唯一索引值重复的异常,我们就可以判定为加锁失败。
再看一遍,这么做能够满足分布式锁需要的特点吗?

  1. 由于唯一索引的特性,数据库实现的锁天然满足
  2. 可重入机制可以通过检查当前获取锁的成员id与竞争者id是否相同来实现
  3. 锁失效机制可以通过定时检查锁的获取时间,超时之后自动释放来实现。

总结下来,借助数据库数据库能够实现分布式锁,并且原理也容易理解,但其中会存在可重入、锁失效之类的问题,在此之上需要添加更多的解决方案,同时对于数据库的操作要实现高可用与高性能也需要考虑。

3.2 基于ZooKeeper实现

3.2.1 什么是ZooKeeper

ZooKeeper是一个分布式服务框架,主要用来解决分布式集群中应用系统的一致性问题,它提供基于类似于文件系统的目录树方式的数据存储。Zookeeper的作用主要是维护和监控存储的数据的状态变化,通过监控数据状态的变化来达到基于数据的集群管理。简单来说,ZooKeeper = 文件系统 + 通知机制。

3.2.2 ZooKeeper的数据存储

ZooKeeper的数据都存储在内存中,如下图所示其存储方式类似于linux文件结构,但与文件结构有两点不同

  1. ZooKeeper数据的每个节点都允许存放数据,而文件系统中的目录不能存放数据,只有底层的文件可以
  2. 每个节点的大小不能超过1M

image.png

zk的节点分为四种:

  • 持久节点节点(PERSISTENT):客户端与ZooKeeper断开链接后仍然存在;可以创建子节点,子节点可以是临时节点也可以是永久节点;不能同名
  • 持久顺序节点点(PERSISTENT_SEQUENTIAL):客户端与ZooKeeper断开链接后节点被删除;可以创建子节点,子节点可以是临时节点也可以是永久节点;同名之后会在后面添加序号
  • 临时节点(EPHEMERAL):客户端与ZooKeeper断开链接后删除;不能创建子节点;不能同名
  • 临时顺序节点(EPHEMERAL):客户端与ZooKeeper断开链接后删除;不能创建子节点;同名之后会在后面添加序号

为什么要这么分呢?我理解是持久节点与临时节点需要区分节点的生命周期(是否和客户端session绑定),是否是顺序节点则负责标识节点是否唯一

在此基础上,ZooKeeper还拥有数据监视机制,可以对某个路径的终节点和子节点的变更进行监视,当其发生变更时就可以调用注册的callback方法来执执行具体的业务逻辑。

3.2.3 ZooKeeper的选主机制

ZooKeeper的目标是高可用,这意味着使用zk的时候往往使用集群而非单点,否则如果单点出故障就会导致服务的不可用,因此zk使用了一套完整的选主机制。
ZooKeeper中包含了Leader、Follwer和Observer三个角色
其中被选举的机器称之为Leader,Follwer和Observer时集群中其他机器节点,区别在于Observer不参与Leader的选举,只参与同步数据,因此不会影响集群的性能,可以无限扩展从而使集群抗住更大的流量。
被选出来的Leader有以下几个左右:

  1. 执行写操作并将操作同步给其他节点
  2. 恢复数据
  3. 维持与follower的心跳,接受follow的请求

leader在服务启动时就会被选举出来,当leader崩溃或者leader失去大多数follower之后,zk就会进入恢复模式,在此期间需要重新选出一个leader,让所有server恢复到正常状态。zk选举的算法是基于Paxos协议
或者Fast Paxos协议实现的,默认基于Fast Paxos,下面对选举的流程进行描述。

在描述之前需要先明确两个概念

  1. zxid: 表示当前节点完成的数据同步情况,这个值越大说明节点的数据同步情况越完整,当选举进行节点之间的比较时,这个值作为第一优先级
  2. myid: 服务节点本身的id,节点比较时的第二优先级,这个值是在zxid相同时保证有唯一的大小关系

现在假设我们当前有4个zk节点(node1、node2、node3、node4),myid分别为1、2、3、4并且他们之间按照这个顺序启动

  1. 初始启动时,所有节点的zxid都为0,所以他之间的选举大小只和myid有关。由于zk选主必须要超过半数服务同意(包括自己)且最低需要3个节点,当启动了node1和node2时还无法进行选举,当node3启动时,满足了最低3节点的要求,zk进入选举状态,由于3的myid最大,选举出3为leader,zk进入运行状态。当node4重启时,尽管4的myid要大于3,但是由于此时zk已经有了Leader且并不在选举状态,因此当前Leader仍为3
  2. 运行过程中,假如node3崩溃了,在其他node发现leader挂掉之后就会进入重新选举。

假如此时node1、node2、node4的zxid分别为2、3、3,并且node1发现leader挂了,重新选举可以分为以下几步

  1. node1发现leader挂了,因此发起重新选举,node1给自己投票,并将zxid(2,1) 发送给node2和node3
  2. node2和node4比较node1和自己的(zxid, myid) 发现自己都比node1要优先,因此将各自的(zxid, myid)发送给node1,node1接收到他俩的回复之后,就会给对他们进行投票
  3. node2和node4在回复完node1之后,会各自开启自己的选主流程,过程和node1一致,此时node2收到node4的请求之后会给node4投票,node4拥有超过一半的投票之后成为主节点,zk集群恢复成可用状况。

从选举流程中也可以看出,由于每台机器在选举时都要给所有其他的机器发送请求,因为参与选举的机器数量与重新选举的耗时息息相关,由于重新选举时zk不可用,耗时低的重新选举对于有着高可用要求的集群来说非常重要。zk官方给出的压测报告是7台zk服务需要大概200ms的选主。这也是为什么会有Observer这个角色,不参与选举的机器可以在不影响选举开销的情况下扩充集群的性能。

3.2.4 ZooKeeper的数据一致性

当执行写操作时,ZooKeeper采用数据广播来保证分布式场景下事物的最终一致性,其流程大致可以描述为。

  1. Follwer接受到来自客户端的写请求,就将其转发给Leader
  2. Leader接受到写请求,并为其分配一个全局唯一的事务ID,称为zxid(单调递增)
  3. Leader生成一个如(zxid, data)的数据提案Proposal, 将将其放入各Follower的FIFO队列中,并按照FIFO策略将Proposal广播
  4. Follower接收到Proposal之后,先将数据落盘,再向Leader回复ACK
  5. 当Leader接收到超过半数的ack之后(包括自己),就向Follower发送commit命令,同时自己在本地commit
  6. Follower接收到commit之后提交事务并向客户端返回结果

这么做就能保证leader和Follower具有相同的系统状态

3.3 基于缓存实现

基于数据库的实现方式虽然直观,但是性能并不够优秀,因此我们将目光移到缓存中间件上。

3.3.1 单机Redis实现

这里使用Redis举例,一个简单的方式是基于单机的Redis来实现一个共享锁服务,形如SET key value NX PX|EX time
其中key是锁名称,value是加锁的用户名称,NX是Redis的语义,表示只有key不存在的时候才能set成功,这就是互斥性的保证,以此来满足锁的基本特性,PX|EX则是一个过期时间,来实现锁的超时机制。
如下图,当user需要获取锁时,则给资源key打上自己的user name, 同时保证只有锁的value和自己的user name相同时锁才能够被释放,以此来保证锁只能被用户自己释放或通过超时释放。
image.png
但这么做最大的风险是什么呢?是当这个Redis实例挂了则意味着整个锁机制失效了,所有使用者无法获取和释放锁,因而无法使用共享资源或者导致共享资源出现数据不一致。

3.3.2 基于Redis的高可用分布式锁——RedLock

RedLock是由Redis作者提出来的高可用分布式锁,由多个独立的Redis节点组成,节点之间完全独立而非类似ZooKeeper的主从关系,并且要求多个节点分开机器部署。
RedLock的高可用基于一个原则:分布式高可用系统中大多数存活即可用。在此基础上,每个单独的节点获取和释放锁的操作,都完全基于单机版的方式。
RedLock的获取锁流程

  1. 获取当前时间T1, 并按照顺序以此向后续的几个独立节点依次获取锁
  • 为什么要按照顺序?防止不同用户在同一时间分别在不同节点获取到锁,造成死锁
  1. 对于每个节点获取锁时,会有一个几十毫秒左右的超时时间,防止获取锁的整体时间变长
  2. 计算获取锁的总时间,并判断是否在大部分节点获取到锁(N/2+1), 如果是则表示锁获取成功,反之需要尽快释放掉锁

RedLock的释放锁流程

  1. 向所有节点发起释放锁操作,不论节点有没有获取成功锁

image.png

4. Chubby锁服务

Chubby是Google研发的针对分布式系统资源管理的粗粒度锁服务,也是书中介绍的一种实现。
Chubby主要原理和ZooKeeper很类似,都是拥有Paxos一致性协议实现的选主机制,文件订阅通知机制,类似文件系统的接口。区别在于

  1. ZooKeeper的选主实现是完全分布的,没有中心管理节点,使得选主时要经过多轮通信和投票来达成最终一致,Chubby增加了一些中心管理策略,在达到一致的目标前提下改善了系统策略。
  2. 所有客户端对Chubby的读写操作都由主控服务器来负责,主控服务器遇到写请求后会更改维护的数据,其他的备份服务器只负责同步数据、转发客户端请求到主控服务器、在主控服务器崩溃后进行接管。这么做更容易维护数据的一致性
posted @ 2022-12-01 23:42  Hugh_Locke  阅读(72)  评论(0编辑  收藏  举报