【分布式系统遨游】分布式互斥与分布式锁

NoSay 2020-06-06 02:15:12
原文地址:https://segmentfault.com/a/1190000022423451

写在前面

在工作学习中,我们常常听说分布式,集群,容器等等名词,但是当学妹们问你什么是分布式的时候,你是否有一种书到用时方恨少的感觉呢?为了在学妹面前“扬眉吐气”一把,今天开始,我们就去会一会分布式,Let's go!

为什么有分布式结构

在我们了解为什么会有分布式结构之前,我们想学习一下什么是分布式结构。分布式就是一个业务分拆多个子业务,部署在不同的服务器上。举个例子,假设需要开发一个在线商城。按照微服务的思想,我们需要按照功能模块拆分成多个独立的服务,如:用户服务、产品服务、订单服务、后台管理服务、数据分析服务等等。这一个个服务都是一个个独立的项目,可以独立运行。如果服务之间有依赖关系,那么通过RPC方式调用。了解了什么是分布式结构,是不是就很自然的总结出一系列的原因,比如解耦和,链路更清晰,职责更明确等等。

那么,分布式之间是如何协调与同步呢?针对这个问题,我们通过以下几个问题去解释。

分布式互斥

首先,我们想象一下,对于多个服务去访问同一资源时,就会出现分布式互斥现象,怎么理解呢?想象一下,你正在一家餐厅使用自助咖啡机泡制咖啡,突然有个人过来挪走了你的杯子,开始泡制他自己的咖啡。你耐着性子等他操作完,继续泡制自己的咖啡。结果你开始没多久,他又回来中断了你泡制咖啡的过程。此时,咖啡机就是一个临界资源,两个人表示两个服务,这种现象就叫做分布式互斥。
那么,我们如何去解决这种服务之前互斥访问临界资源的现象呢?

集中式算法

首先,我们最先想到的肯定是增加一个“管理员”,约束大家不要插入别人使用咖啡机的时间,那么做好一个管理员,我们就需要一套完整的管理体系,例如我们打卡上下班一样,放到计算机世界来说,这里就需要一个算法来协调,这就是分布式互斥算法,每个程序在需要访问临界资源时,先给协调者发送一个请求。如果当前没有程序使用这个资源,协调者直接授权请求程序访问;否则,按照先来后到的顺序为请求程序“排一个号”。如果有程序使用完资源,则通知协调者,协调者从“排号”的队列里取出排在最前面的请求,并给它发送授权消息。拿到授权消息的程序,可以直接去访问临界资源。
这样的算法我们给他起个名字就叫做”集中式算法”,集中式算法大概的流程是:存在一个协调者,在程序进行临界资源的访问的时候,首先要向协调者发送请求授权信息,如果存在有程序正在使用,则将其放入等待队列,等到正在执行的程序释放资源,协调者就去等待队列中取出一个程序发放授权信息,程序执行完毕后释放。那么这个算法的优缺点就很明显了,优点在于简单,直观,容易实现且只需要和协调者进行通信。缺点就在于协调者会成为系统的性能瓶颈,协调者处理的消息数量会随着需要访问临界资源的程序数量线性增加,并且容易引发单点故障问题。当然,对于故障问题我们可以采用主备备份的方式来解决,所以,集中式算法可以适用于比较广泛的应用场景。![集中式算法.png](https://segmentfault.com/img/bVbGftq "集中式算法.png")

民主协商算法

那么既然引入协调者会带来一些问题,这时你可能就会想,不用协调者是否可以实现对临界资源的互斥访问呢?想象一下,当你需要使用自助咖啡机的时候,是不是可以先去征求其他人的意见,在确认其他人都没在使用也暂时不会使用咖啡机时,你就可以放心大胆地去泡制自己的咖啡了呢?
同理,我们可以把这套算法用于分布式系统。当一个程序要访问临界资源时,先向系统中的其他程序发送一条请求消息,在接收到所有程序返回的同意消息后,才可以访问临界资源。其中,请求消息需要包含所请求的资源、请求者的 ID,以及发起请求的时间。这就是“民主协商算法“,仔细品一下就会发现一个程序在对临界资源进行访问的时候就要进行2*(N-2)条消息,在大型系统中使用分布式算法,消息数量会随着需要访问临界资源的程序数量呈指数级增加,容易导致高昂的“沟通成本,并且当一个节点出现宕机或者其它异常情况的时候会让其它获取资源的程序陷入一直等待的处境,虽然我们可以加上一个监控异常的方式,但是对于每个程序进行故障检测,无疑增加了更大的复杂性。所以,民主协商适用于节点数目少且变动不频繁的系统。
![协商算法.png](https://segmentfault.com/img/bVbGfts "协商算法.png")

令牌环算法

我们能不能避免这种多通信呢?当然可以,集中式算法就可以啊,但是我们不想新增一个协调者,这可怎么办呢?下边我们来讲述一下”令牌环算法“,令牌环方法的实现类似于华为的轮值CEO模式(任老板不久刚换下来么),所有程序构成一个环结构,令牌按照顺时针(或逆时针)方向在程序之间传递,收到令牌的程序有权访问临界资源,访问完成后将令牌传送到下一个程序;若该程序不需要访问临界资源,则直接把令牌传送给下一个程序。无人机通信就是一个很直白的例子。
他解决了什么问题呢?集中式和分布式都存在单点故障的问题,在令牌环中,若某一个程序出现故障,则直接将令牌传递给故障程序的下一个程序,从而很好地解决单点故障问题,提高系统的健壮性,带来更好的可用性。当然,他也存在一定的弊端,就是一个程序无论有没有需求都会进行排队,这就降低了系统的实时性。适用于系统规模较小,并且系统中每个程序使用临界资源的频率高且使用时间比较短的场景。

可以看到,上面提到的集中式、分布式和令牌环 3 个互斥算法,都不适用于规模过大、节点数量过多的系统。那么,什么样的互斥算法适用于大规模系统呢?现在有一个很流行的互斥算法,就是两层结构的分布式令牌环算法,对于节点很多的系统,我们分成不同的管理环,然后根据管理环的内容再划分不同的节点环。从而达到分治的目的,以此来提高性能。
![令牌环算法.png](https://segmentfault.com/img/bVbGftx "令牌环算法.png")

分布式锁

在讲述了分布式互斥算法之后,我们主要了解了如何协调进程获取权限以及根据权限有序访问共享资源,但是这里边我们忽略了一个词--权限,这个权限是怎么产生的呢?工作原理是什么?接下来,我们就一起来看下,我们是如何控制权限的。
在之前的文章我们有提到过在访问公用资源的时候需要加锁来保证数据一致性,那么对于分布式下的权限我们是否也可以用锁来实现的呢?当然可以,接下来我们来看看分布式锁。
首先,我们知道,锁是实现多线程同时访问同一共享资源,保证同一时刻只有一个线程可访问共享资源所做的一种标记。对于单机来说,锁是可以独立维护的,但是对于分布式来说,这种方式显然不太适用,因为单个机器的线程锁无法管控到多个机器对统一资源的访问。那么问题就来了,我们是不是也需要有一个地方去维护这个锁呢?分布式中锁被存在公共存储(比如 Redis、Memcache、数据库等三方存储中),以实现多个进程并发访问同一个临界资源,同一时刻只有一个进程可访问共享资源,确保数据的一致性。我们来看一看以下三种实验方式。

基于数据库实现分布式锁

这里的数据库指的是关系型数据库。创建一张锁表,然后通过操作该表中的数据来实现。当我们要锁住某个资源时,就在该表中增加一条记录,想要释放锁的时候就删除这条记录。数据库对共享资源做了唯一性约束,如果有多个请求被同时提交到数据库的话,数据库会保证只有一个操作可以成功,操作成功的那个线程就获得了访问共享资源的锁,可以进行操作。这种可以达到分布式锁的目的,而且实现非常简单,但是因为所有的数据都是在落在硬盘上,肯定会导致IO开销增大,性能变差,所以这种方式只适用于并发量低并且对性能要求低的场景。而且他会存在单点故障和死锁的问题,如果一个程序在获取锁之后挂掉,就会导致其他进程一直获取不到锁。
![数据库分布式锁.png](https://segmentfault.com/img/bVbGftJ "数据库分布式锁.png")

基于缓存实现分布式锁

既然数据库性能限制了业务的并发,那么我们就试试缓存,把数据放在计算机内存中,不用写入磁盘,减少IO读写,最常见的就是用Redis的setnx来实现,通过维护进程访问共享资源的先后顺序来协调锁资源,而且大家都知道,redis的key可以设置过期时间,这就可以解决死锁的问题。我们总结一下它的优点,性能更好,很多缓存可以跨集群部署,避免单点故障,可以设置控制锁的超时时间。缺点呢?缺点就在于当一个死锁产生时,它需要等待锁超时释放,这就导致了一种场景,12点的时候用户购买商品购买失败,但是到12.30又可以购买了(假如过期时间在30分钟后),或者说一个进程本来执行时间就长,当过期时间小于它的执行时间时,不正确的释放了锁。

基于Zookeeper实现分布式锁

上边的两种方式好像都不太符合我们的预期,有没有更好的办法呢?接下来我们看一看Zookeeper对于分布式锁的实现。ZooKeeper 基于临时顺序节点实现分布式锁,来解决多个进程同时访问同一临界资源时,数据的一致性问题,通常用于配置管理,名字服务,提供分布式同步以及集群管理。它维护了一个临时节点的存储结构,当客户端与 ZooKeeper 断开连接后,该进程创建的临时节点就会被删除。这块可能不太容易理解,我们举一个例子来帮助大家理解:假设用户 A、B、C 同时在 11 月 11 日的零点整提交了购买吹风机的请求,ZooKeeper 会采用如下方法来实现分布式锁:

  1. 在与该方法对应的持久节点 shared_lock 的目录下,为每个进程创建一个临时顺序节点。如下图所示,吹风机就是一个拥有 shared_lock 的目录,当有人买吹风机时,会为他创建一个临时顺序节点。
  2. 每个进程获取 shared_lock 目录下的所有临时节点列表,注册子节点变更的Watcher,并监听节点。
  3. 每个节点确定自己的编号是否是 shared_lock 下所有子节点中最小的,若最小,则获得锁。例如,用户 A 的订单最先到服务器,因此创建了编号为 1 的临时顺序节点LockNode1。该节点的编号是持久节点目录下最小的,因此获取到分布式锁,可以访问临界资源,从而可以购买吹风机。
  4. 若本进程对应的临时节点编号不是最小的,则分为两种情况:
    a. 本进程为读请求,如果比自己序号小的节点中有写请求,则等待。
    b. 本进程为写请求,如果比自己序号小的节点中有读请求,则等待。

可以看到,使用 ZooKeeper 可以完美解决设计分布式锁时遇到的各种问题,比如单点故障、不可重入、死锁(临时节点的特性)等问题。虽然 ZooKeeper 实现的分布式锁,几乎能涵盖所有分布式锁的特性,且易于实现,但需要频繁地添加和删除节点,所以性能不如基于缓存实现的分布式锁。
![Zookeeper.png](https://segmentfault.com/img/bVbGftP "Zookeeper.png")

下期预告

【分布式系统遨游】分布式经典架构

关注我们

欢迎对本系列文章感兴趣的读者订阅我们的公众号,关注博主下次不迷路~

Nosay

声明:该文章系转载,转载该文章的目的在于更广泛的传递信息,并不代表本网站赞同其观点,文章内容仅供参考。

本站是一个个人学习和交流平台,网站上部分文章为网站管理员和网友从相关媒体转载而来,并不用于任何商业目的,内容为作者个人观点, 并不代表本网站赞同其观点和对其真实性负责。

我们已经尽可能的对作者和来源进行了通告,但是可能由于能力有限或疏忽,导致作者和来源有误,亦可能您并不期望您的作品在我们的网站上发布。我们为这些问题向您致歉,如果您在我站上发现此类问题,请及时联系我们,我们将根据您的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。