2020-08-27

典型分布式锁的实现
如何协调多个进程获取权限和根据权限有序访问共享资源,“获得访问权限的进程可以访问共享资源,其他进程必须等待拥有该权限的进程释放权限”。设置或者产生这个权限的原理就是分布式锁的应用。
锁是实现多线程同时访问同一共享资源,保证同一时刻只有一个线程可访问共享资源所做的一种标记。解决的是资源竞争的问题。

实现分布式锁的 3 种主流方法,即:
基于数据库实现分布式锁,这里的数据库指的是关系型数据库;
基于缓存实现分布式锁;
基于 ZooKeeper 实现分布式锁。

基于数据库实现分布式锁
要实现分布式锁,最简单的方式就是创建一张锁表,然后通过操作该表中的数据来实现。当我们要锁住某个资源时,就在该表中增加一条记录,想要释放锁的时候就删除这条记录。数据库对共享资源做了唯一性约束,如果有多个请求被同时提交到数据库的话,数据库会保证只有一个操作可以成功,操作成功的那个线程就获得了访问共享资源的锁,可以进行操作。基于数据库实现的分布式锁,是最容易理解的。但是,因为数据库需要落到硬盘上,频繁读取数据库会导致 IO 开销大,因此这种分布式锁适用于并发量低,对性能要求低的场景。

基于数据库实现分布式锁比较简单,绝招在于创建一张锁表,为申请者在锁表里建立一条记录,记录建立成功则获得锁,消除记录则释放锁。
该方法依赖于数据库,主要有两个缺点:
单点故障问题。一旦数据库不可用,会导致整个系统崩溃。
死锁问题。数据库锁没有失效时间,未获得锁的进程只能一直等待已获得锁的进程主动释放锁。一旦已获得锁的进程挂掉或者解锁操作失败,会导致锁记录一直存在数据库中,其他进程无法获得锁。

基于缓存实现分布式锁
数据库的性能限制了业务的并发量,那么对于需求量激增的场景是否有解决方法呢?基于缓存实现分布式锁的方式,非常适合解决这种场景下的问题。所谓基于缓存,也就是说把数据存放在计算机内存中,不需要写入磁盘,减少了 IO 读写。接下来,我以 Redis 为例与你展开这部分内容。Redis 通常可以使用 setnx(key, value) 函数来实现分布式锁。key 和 value 就是基于缓存的分布式锁的两个属性,其中 key 表示锁 id,value = currentTime + timeOut,表示当前时间 + 超时时间。也就是说,某个进程获得 key 这把锁后,如果在 value 的时间内未释放锁,系统就会主动释放锁。setnx 函数的返回值有 0 和 1:返回 1,说明该服务器获得锁,setnx 将 key 对应的 value 设置为当前时间 + 锁的有效时间。返回 0,说明其他服务器已经获得了锁,进程不能进入临界区。该服务器可以不断尝试 setnx 操作,以获得锁。

Redis 通过队列来维持进程访问共享资源的先后顺序。Redis 锁主要基于 setnx 函数实现分布式锁,当进程通过 setnx<key,value> 函数返回 1 时,表示已经获得锁。排在后面的进程只能等待前面的进程主动释放锁,或者等到时间超时才能获得锁。

相对于基于数据库实现分布式锁的方案来说,基于缓存实现的分布式锁的优势表现在以下几个方面:性能更好。数据被存放在内存,而不是磁盘,避免了频繁的 IO 操作。很多缓存可以跨集群部署,避免了单点故障问题。很多缓存服务都提供了可以用来实现分布式锁的方法,比如 Redis 的 setnx 方法等。可以直接设置超时时间来控制锁的释放,因为这些缓存服务器一般支持自动删除过期数据。这个方案的不足是,通过超时时间来控制锁的失效时间,并不是十分靠谱,因为一个进程执行时间可能比较长,或受系统进程做内存回收等影响,导致时间超时,从而不正确地释放了锁。为了解决基于缓存实现的分布式锁的这些问题,我们再来看看基于 ZooKeeper 实现的分布式锁吧。

基于 ZooKeeper 实现分布式锁

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

持久节点。这是默认的节点类型,一直存在于 ZooKeeper 中。
持久顺序节点。也就是说,在创建节点时,ZooKeeper 根据节点创建的时间顺序对节点进行编号。
临时节点。与持久节点不同,当客户端与 ZooKeeper 断开连接后,该进程创建的临时节点就会被删除。
临时顺序节点,就是按时间顺序编号的临时节点。
根据它们的特征,ZooKeeper 基于临时顺序节点实现了分布锁。

以电商售卖吹风机的场景为例。假设用户 A、B、C 同时在 11 月 11 日的零点整提交了购买吹风机的请求,ZooKeeper 会采用如下方法来实现分布式锁:

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

使用 ZooKeeper 可以完美解决设计分布式锁时遇到的各种问题,比如单点故障、不可重入、死锁等问题。虽然 ZooKeeper 实现的分布式锁,几乎能涵盖所有分布式锁的特性,且易于实现,但需要频繁地添加和删除节点,所以性能不如基于缓存实现的分布式锁。

ZooKeeper 分布式锁的可靠性最高,有封装好的框架,很容易实现分布式锁的功能,并且几乎解决了数据库锁和缓存式锁的不足,因此是实现分布式锁的首选方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值