分布式技术架构原理解析之协调与同步(五)分布式锁

前言

前面的文章详细介绍了“分布式互斥”,解释了同一临界资源(共享资源)同一时刻只能被一个程序访问的问题,也就是说只有获得访问权限的进程才可以访问共享资源,而此时其他进程必须等待拥有该权限的进程释放权限。那么在访问共享资源时,这个权限是如何设置或产生的呢?以及设置或产生这个权限的工作原理是什么?本文就来介绍分布式锁是如何解决这个问题的。

1、分布式锁介绍

1.1 什么是锁

通常在单机多线程环境中,会有多个线程访问同一个共享资源(临界资源)的情况,此时,为了保证数据的一致性,会有某种机制来控制让满足某条件的线程访问资源,不满足的等待,直到下一轮竞争中满足条件后才可访问资源。
而这个机制就是:为了实现分布式互斥,需要在某处设置一个所有线程都能看到的标记,当标记被设置后,其他线程只能等待拥有该标记的线程执行完成,并释放该标记后,才能去设置该标记和访问共享资源。这个标记,就是我们常说的
总结:在单机环境中锁是实现多线程访问同一共享资源时,保证同一时刻只有一个线程可访问该资源而设置的一种标记

1.2 分布式锁与单机环境锁的区别

分布式锁为了实现多个进程并发访问同一个临界资源,同一时刻只有一个进程可访问共享资源,确保数据的一致性。而与单机环境下锁的主要区别是:

  1. 运行在分布式环境:分布式锁是在分布式环境下,系统部署在多个机器中,实现多进程分布式互斥的一种锁;
  2. 锁被公共存储:为了保证多个进程能看到锁,锁被存在公共存储(比如 Redis、Memcache、数据库等三方存储中);

1.3 分布式锁应用场景

我们先看一个实际场景的例子:某电商要售卖某大牌手机,库存只有 2 个,但有多个来自不同地区的用户几乎同时下单,如果规定按照下单时间作为购买成功的判断依据,最终需要保证手机售出时,数据库中更新的库存是正确的。
**解决上述问题单机环境锁的方案是:**给手机的库存数加一个锁。当有一个用户提交订单后,后台服务器给库存数加一个锁,根据该用户的订单修改库存。而其他用户必须等到锁释放以后,才能重新获取库存数,继续购买。在这里,手机的库存就是共享资源,不同的购买者对应着多个进程,后台服务器对共享资源加的锁就是告诉其他进程“非请勿入”。
但是上述方法能够解决问题吗?当然没这么简单。想象一下,用户 A 想买 1 个手机,用户 B 想买 2 个手机。在理想状态下,用户 A 网速好先买走了 1 个,库存还剩下 1 个,此时应该提示用户 B 库存不足,用户 B 购买失败。但实际情况是,用户 A 和用户 B 同时获取到商品库存还剩 2 个,用户 A 买走 1 个,在用户 A 更新库存之前,用户 B 又买走了 2 个,此时用户 B 更新库存,商品还剩 0 个。这时,电商就头大了,总共 2 个吹风机,却卖出去了 3 个。
所以,如果只使用单机锁将会出现不可预知的后果。因此,在高并发场景下,为了保证临界资源同一时间只能被一个进程使用,从而确保数据的一致性,我们就需要引入分布式锁了。此外,在大规模分布式系统中,单个机器的线程锁无法管控多个机器对同一资源的访问,这时使用分布式锁,就可以把整个集群当作一个应用一样去处理,实用性和扩展性更好。

1.4 分布式锁的实现方法

实现分布式锁的 3 种主流方法:

  • 基于数据库实现分布式锁,这里的数据库指的是关系型数据库;
  • 基于缓存实现分布式锁;
  • 基于 ZooKeeper 实现分布式锁;
1.4.1 基于数据库实现分布式锁

基于数据库实现分布式锁的方式就是创建一张锁表,然后通过操作该表中的数据来实现。当我们要锁住某个资源时,就在该表中增加一条记录,想要释放锁的时候就删除这条记录。数据库对共享资源做了唯一性约束,如果有多个请求被同时提交到数据库的话,数据库会保证只有一个操作可以成功,操作成功的那个线程就获得了访问共享资源的锁,可以进行操作。
实现过程举例:
下面,我们还是以上面电商售卖手机的场景为例。手机库存是 2 个,有 3 个来自不同地区的用户{A,B,C}想要购买,其中用户 A 想买 1 个,用户 B 想买 2 个,用户 C 想买 1 个。具体实现过程如下:

  • 用户 A 和用户 B 几乎同时下单,但用户 A 的下单请求最先到达服务器。

  • 该商家的产品数据库中增加了一条关于用户 A 的记录,用户 A 获得了锁,他的订单请求被处理,服务器修改手机库存数,减去 1 后还剩下 1 个。

  • 当用户 A 的订单请求处理完成后,有关用户 A 的记录被删除,服务器开始处理用户 B 的订单请求。

  • 这时,库存只有 1 个了,无法满足用户 B 的订单需求,因此用户 B 购买失败。

  • 从数据库中,删除用户 B 的记录,服务器开始处理用户 C 的订单请求,库存中 1 个吹风机满足用户 C 的订单需求。

  • 数据库中增加了一条关于用户 C 的记录,用户 C 获得了锁,他的订单请求被处理,服务器修改吹风机数量,减去 1 后还剩下 0 个。
    在这里插入图片描述
    小结:
    基于数据库实现的分布式锁比较简易,绝招在于创建一张锁表,为申请者在锁表里建立一条记录,记录建立成功则获得锁,消除记录则释放锁。
    通常实际应用在局部场景中使用数据库锁实现对资源的互斥访问。
    该方法依赖于数据库,主要有两个缺点

  • 单点故障问题:一旦数据库不可用,会导致整个系统崩溃。

  • 死锁问题:数据库锁没有失效时间,未获得锁的进程只能一直等待已获得锁的进程主动释放锁。一旦已获得锁的进程挂掉或者解锁操作失败,会导致锁记录一直存在数据库中,其他进程无法获得锁。

1.4.2 基于缓存实现分布式锁

基于数据库实现的分布式锁,是最容易理解的。但是,因为数据库需要落到硬盘上,频繁读取数据库会导致 IO 开销大,因此这种分布式锁适用于并发量低,对性能要求低的场景。
对于双 11、双 12 等需求量激增的场景,数据库锁是无法满足其性能要求的,而基于缓存实现分布式锁的方式,非常适合解决这种场景下的问题。所谓基于缓存,也就是说把数据存放在计算机内存中,不需要写入磁盘,减少了 IO 读写。
Redis-基于缓存实现分布式锁的方式:
Redis 通常可以使用 setnx(key, value) 函数来实现分布式锁。keyvalue 就是基于缓存的分布式锁的两个属性,其中 key 表示锁 id,value = currentTime + timeOut,表示当前时间 + 超时时间。也就是说,某个进程获得 key 这把锁后,如果在 value 的时间内未释放锁,系统就会主动释放锁setnx 函数的返回值有 0 和 1:

  • 返回 1:说明该服务器获得锁,setnx 将 key 对应的 value 设置为当前时间 + 锁的有效时间。

  • 返回0:说明其他服务器已经获得了锁,进程不能进入临界区。该服务器可以不断尝试 setnx 操作,以获得锁。
    实现过程举例:
    仍然以电商售卖手机的场景为例,说明基于缓存实现的分布式锁,假设现在库存数量是足够的。实现过程如下:

  • 用户 A 的请求因为网速快,最先到达 Server2,setnx 操作返回 1,并获取到购买吹风机的锁;

  • 用户 B 和用户 C的请求,几乎同时到达了 Server1 和 Server3,但因为这时 Server2获取到了吹风机数据的锁,所以只能加入等待队列。

  • Server2 获取到锁后,负责管理吹风机的服务器执行业务逻辑,只用了 1s就完成了订单。

  • 订单请求完成后,删除锁的 key,从而释放锁。

  • 此时,排在第二顺位的 Server1获得了锁,可以访问吹风机的数据资源。

  • 但不巧的是,Server1 在完成订单后发生了故障,无法主动释放锁。

  • 于是,排在第三顺位的 Server3 只能等设定的有效时间(比如 30 分钟)到期,锁自动释放后,才能访问吹风机的数据资源,也就是说用户 C 只能到00:30:01 以后才能继续抢购。
    在这里插入图片描述
    小结:
    总结来说,Redis 通过队列来维持进程访问共享资源的先后顺序。Redis 锁主要基于 setnx 函数实现分布式锁,当进程通过 setnx<key,value> 函数返回 1 时,表示已经获得锁。排在后面的进程只能等待前面的进程主动释放锁,或者等到时间超时才能获得锁。
    基于缓存实现的分布式锁的优势表现在以下几个方面:

  • 性能更好:数据被存放在内存,而不是磁盘,避免了频繁的 IO 操作。

  • 避免单独故障:很多缓存可以跨集群部署,避免了单点故障问题。

  • 避免死锁:可以直接设置超时时间来控制锁的释放,因为这些缓存服务器一般支持自动删除过期数据。
    这个方案的不足是:

  • 通过超时时间来控制锁的失效时间,并不是十分靠谱,因为一个进程执行时间可能比较长,或受系统进程做内存回收等影响,导致时间超时,从而不正确地释放了锁。

1.4.3 基于 ZooKeeper 实现分布式锁

ZooKeeper 基于树形数据存储结构实现分布式锁,来解决多个进程同时访问同一临界资源时,数据的一致性问题。ZooKeeper 的树形数据存储结构主要由 4 种节点构成:

  • 持久节点:这是默认的节点类型,一直存在于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. 本进程为写请求,如果比自己序号小的节点中有读请求,则等待。
    例如,用户 B 也想要买手机,但在他之前,用户 C 想看看手机的库存量。因此,用户 B 只能等用户 A 买完手机、用户 C 查询完库存量后,才能购买手机。
    在这里插入图片描述
    小结:
    使用 ZooKeeper 可以完美解决设计分布式锁时遇到的各种问题,比如单点故障、不可重入、死锁等问题。虽然 ZooKeeper 实现的分布式锁,几乎能涵盖所有分布式锁的特性,且易于实现,但需要频繁地添加和删除节点,所以性能不如基于缓存实现的分布式锁。
1.4.4 三种分布式锁实现方式对比

在这里插入图片描述
总结来说,ZooKeeper 分布式锁的可靠性最高,有封装好的框架,很容易实现分布式锁的功能,并且几乎解决了数据库锁和缓存式锁的不足,因此是实现分布式锁的首选方法。

3、总结

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
分布式服务架构:原理、设计与实战pdf》是一本介绍分布式服务架构的书籍,以下是我对这本书的回答。 这本书是关于分布式服务架构的原理、设计和实战的指南。分布式服务架构是一种软件架构模式,它通过将一个应用程序分解为多个独立的服务来处理不同的业务逻辑。每个服务都可以独立部署、扩展和管理,通过网络进行通信和协作。 在这本书中,作者首先介绍了分布式服务架构的基本原理和概念。他们解释了为什么分布式服务架构在现代软件开发中如此重要,以及如何将其与传统的单体应用程序架构进行对比。接着,他们详细讨论了设计分布式服务架构的一些关键问题,如服务的边界划分、通信机制、数据一致性和容错机制等。同时,他们还介绍了一些常用的分布式服务框架和工具,如Dubbo、Spring Cloud和Kubernetes等。 在实战部分,作者提供了一些实际案例和应用场景,展示了如何应用分布式服务架构来解决现实世界中的问题。他们从不同的角度和维度分析了这些案例,包括性能优化、系统可用性、弹性伸缩和容灾等。通过这些案例,读者可以更好地理解和应用分布式服务架构。 总的来说,这本书在分布式服务架构的原理、设计和实战方面提供了全面的指导。无论是对于初学者还是有一定经验的开发人员来说,这本书都是一个宝贵的参考资料。它不仅介绍了基本原理和关键概念,还提供了一些实际的案例和经验教训。通过阅读这本书,读者可以更好地理解和应用分布式服务架构,从而提高软件开发的效率和质量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值