您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

时间:2020-06-15 10:19:52  来源:  作者:
这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

什么是分布式锁?

大家好,我是jack xu,今天跟大家聊一聊分布式锁。首先说下什么是分布式锁,当我们在进行下订单减库存,抢票,选课,抢红包这些业务场景时,如果在此处没有锁的控制,会导致很严重的问题。学过多线程的小伙们知道,为了防止多个线程同时执行同一段代码,我们可以用 synchronized 关键字或 JUC 里面的 ReentrantLock 类来控制,但是目前几乎任何一个系统都是部署多台机器的,单机部署的应用很少,synchronized 和 ReentrantLock 发挥不出任何作用,此时就需要一把全局的锁,来代替 JAVA 中的 synchronized 和 ReentrantLock。

分布式锁的实现方式流行的主要有三种,分别是基于缓存 redis 的实现方式,基于 zk 临时顺序节点的实现以及基于数据库行锁的实现。我们先来说下用 Jedis 中的 setnx 命令来构建这把锁。

Jedis写法

使用 Redis 做分布式锁的思路是,在 redis 中设置一个值表示加了锁,然后释放锁的时候就把这个 key 删除。思路是很简单,但是在使用过程中要避免一些坑,我们先看下加锁的代码:

    /**
     * 尝试获取分布式锁
     *
     * @param jedis      Redis客户端
     * @param lockKey    锁
     * @param requestId  请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        // set支持多个参数 NX(not exist) XX(exist) EX(seconds) PX(million seconds)
        String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
        if (LOCK_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

这段代码很简单,主要说下这里用的命令是 SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL],而没有使用 SETNX+EXPIRE 的命令,原因是 SETNX+EXPIRE 是两条命令无法保证原子性,而 SET 是原子操作。那这里为什么要设置超时时间呢?原因是当一个客户端获得了锁在执行任务的过程中挂掉了,来不及显式地释放锁,这块资源将会永远被锁住,这将会导致死锁,所以必须设置一个超时时间

释放锁的代码如下:

    /**
     * 释放分布式锁
     *
     * @param jedis     Redis客户端
     * @param lockKey   锁
     * @param requestId 请求标识,当前工作线程线程的名称
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }

这里也有两点注意的地方,第一是解铃还须系铃人,怎么理解呢,就是 A 加的锁 B 不能去 del 掉吧,不然岂不是全乱套了,谁加的锁就谁去解,我们一般把 value 设为当前线程的 Id,Thread.currentThread().getId(),然后在删的时候判断下是不是当前线程。第二点是验证和释放锁是两个独立操作,不是原子性,这个怎么解决呢?使用 Lua 脚本,即 if redis.call('get', KEYS[1]) == ARGV[1] then returnredis.call('del', KEYS[1]) else return 0 end,它能给我们保证原子性。

Redisson写法

Redisson 是 Java 的 Redis 客户端之一,提供了一些 API 方便操作 Redis。但是 Redisson 这个客户端可有点厉害,我们先打开官网看下 github.com/redisson/re…

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

这个目录里面有很多的功能,Redisson 跟 Jedis 定位不同,它不是一个单纯的 Redis 客户端,而是基于 Redis 实现的分布式的服务,我们可以看到还有 JUC 包下面的类名,Redisson 帮我们搞了分布式的版本,比如 AtomicLong,直接用 RedissonAtomicLong 就行了。锁只是它的冰山一角,并且它对主从,哨兵,集群等模式都支持,当然了,单节点模式肯定是支持的。

在 Redisson 里面提供了更加简单的分布式锁的实现,我们来看下它的用法,相当的简单,两行代码搞定,比 Jedis 要简单的多,而且在 Jedis 里需要考虑的问题,它都已经帮我们封装好了。

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

我们来看下,这里获取锁有很多种的方式,有公平锁有读写锁,我们使用的是 redissonClient.getLock, 这是一个可重入锁。

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

现在我把程序启动一下

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

打开 Redis Desktop Manager 工具,看下到底它存的是什么。原来在加锁的时候,写入了一个 HASH 类型的值,key 是锁名称 jackxu,field 是线程的名称,而 value 是 1(即表示锁的重入次数)。

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

小伙伴可能觉得我在一派胡言,没关系,我们点进去看下它的源码是具体实现的。

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

点进 tryLock() 方法的 tryAcquire() 方法,再到->tryAcquireAsync() 再到->tryLockInnerAsync(),终于见到庐山真面目了,原来它最终也是通过 Lua 脚本来实现的。

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

现在我把这段Lua脚本拿出来分析一下,很简单。

// KEYS[1] 锁名称 updateAccount
// ARGV[1] key 过期时间 10000ms
// ARGV[2] 线程名称
// 锁名称不存在
if (redis.call('exists', KEYS[1]) == 0) then
// 创建一个 hash,key=锁名称,field=线程名,value=1
redis.call('hset', KEYS[1], ARGV[2], 1);
// 设置 hash 的过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
// 锁名称存在,判断是否当前线程持有的锁
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
// 如果是,value+1,代表重入次数+1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
// 重新获得锁,需要重新设置 Key 的过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
// 锁存在,但是不是当前线程持有,返回过期时间(毫秒)
return redis.call('pttl', KEYS[1]);

unlock() 中的 unlockInnerAsync() 释放锁,同样也是通过 Lua 脚本实现。

// KEYS[1] 锁的名称 updateAccount
// KEYS[2] 频道名称 redisson_lock__channel:{updateAccount}
// ARGV[1] 释放锁的消息 0
// ARGV[2] 锁释放时间 10000
// ARGV[3] 线程名称
// 锁不存在(过期或者已经释放了)
if (redis.call('exists', KEYS[1]) == 0) then
// 发布锁已经释放的消息
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;
// 锁存在,但是不是当前线程加的锁
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;

// 锁存在,是当前线程加的锁
// 重入次数-1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
// -1 后大于 0,说明这个线程持有这把锁还有其他的任务需要执行
if (counter > 0) then
// 重新设置锁的过期时间
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
// -1 之后等于 0,现在可以删除锁了
redis.call('del', KEYS[1]);
// 删除之后发布释放锁的消息
redis.call('publish', KEYS[2], ARGV[1]);
return 1;
end;

// 其他情况返回 nil
return nil;

看完它的使用后,我们发现真的使用起来像 JDK 中的 ReentrantLock 一样丝滑。

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

RedLock

RedLock 的中文是直译过来的,就叫红锁。红锁并非是一个工具,而是 Redis 官方提出的一种分布式锁的算法。我们知道如果采用单机部署模式,会存在单点问题,只要 redis 故障了,加锁就不行了。如果采用 master-slave 模式,加锁的时候只对一个节点加锁,即便通过 sentinel 做了高可用,但是如果 master 节点故障了,发生主从切换,此时就会有可能出现锁丢失的问题。基于以上的考虑,其实 redis 的作者 Antirez 也考虑到这个问题,他提出了一个 RedLock 的算法。

我在这里画了一个图,图中这五个实例都是独自部署的,没有主从关系,它们就是5个 master 节点。

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

通过以下步骤获取一把锁:

  • 获取当前时间戳,单位是毫秒
  • 轮流尝试在每个 master 节点上创建锁,过期时间设置较短,一般就几十毫秒
  • 尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n / 2 +1)
  • 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了
  • 要是锁建立失败了,那么就依次删除这个锁
  • 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁

但是这样的这种算法还是颇具争议的,可能还会存在不少的问题,无法保证加锁的过程一定正确。Martin Kleppmann 针对这个算法提出了质疑,接着 antirez 又回复了 Martin Kleppmann 的质疑。一个是很有资历的分布式架构师,一个是 Redis 之父,这个就是著名的关于红锁的神仙打架事件。

最后 Redisson 官网上也给出了如何使用红锁 redlock,几行代码搞定,依然很丝滑,感兴趣的小伙伴可以看下。

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

Zookeeper写法

在介绍 zookeeper 实现分布式锁的机制之前,先粗略介绍一下 zk 是什么东西: zk 是一种提供配置管理、分布式协同以及命名的中心化服务。它的模型是这样的:包含一系列的节点,叫做znode,就好像文件系统一样每个 znode 表示一个目录,然后 znode 有一些特性,我们可以把它们分为四类:

  • 持久化节点(zk断开节点还在)
  • 持久化顺序节点(如果是第一个创建的子节点,那么生成的子节点为/lock/node-0000000000,下一个节点则为/lock/node-0000000001,依次类推)
  • 临时节点(客户端断开后节点就删除了)
  • 临时顺序节点

zookeeper分布式锁恰恰应用了临时顺序节点,下面我们就用图解的方式来看下是怎么实现的。

获取锁

首先,在 Zookeeper 当中创建一个持久节点 ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1。

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

之后,Client1 查找 ParentLock 下面所有的临时顺序节点并排序,判断自己所创建的节点 Lock1 是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

这时候,如果再有一个客户端 Client2 前来获取锁,则在 ParentLock 下再创建一个临时顺序节点Lock2。

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

Client2 查找 ParentLock 下面所有的临时顺序节点并排序,判断自己所创建的节点 Lock2 是不是顺序最靠前的一个,结果发现节点 Lock2 并不是最小的。

于是,Client2 向排序仅比它靠前的节点 Lock1 注册 Watcher,用于监听 Lock1 节点是否存在。这意味着 Client2 抢锁失败,进入了等待状态。

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

这时候,如果又有一个客户端 Client3 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock3。

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

Client3 查找 ParentLock 下面所有的临时顺序节点并排序,判断自己所创建的节点 Lock3 是不是顺序最靠前的一个,结果同样发现节点 Lock3 并不是最小的。

于是,Client3 向排序仅比它靠前的节点 Lock2 注册 Watcher,用于监听 Lock2 节点是否存在。这意味着 Client3 同样抢锁失败,进入了等待状态。

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

这样一来,Client1 得到了锁,Client2 监听了 Lock1,Client3 监听了 Lock2。这恰恰形成了一个等待队列,很像是 Java 当中 ReentrantLock 所依赖的 AQS(AbstractQueuedSynchronizer)。

释放锁

释放锁分为两种情况:

1.任务完成,客户端显示释放

当任务完成时,Client1 会显示调用删除节点 Lock1 的指令。

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

2.任务执行过程中,客户端崩溃

获得锁的 Client1 在任务执行过程中,如果 Duang 的一声崩溃,则会断开与 Zookeeper 服务端的链接。根据临时节点的特性,相关联的节点 Lock1 会随之自动删除。

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

由于 Client2 一直监听着 Lock1 的存在状态,当 Lock1 节点被删除,Client2 会立刻收到通知。这时候 Client2 会再次查询 ParentLock 下面的所有节点,确认自己创建的节点 Lock2 是不是目前最小的节点。如果是最小,则 Client2 顺理成章获得了锁。

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

同理,如果 Client2 也因为任务完成或者节点崩溃而删除了节点 Lock2,那么 Client3 就会接到通知。

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

最终,Client3 成功得到了锁。

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

Curator

Apache 的开源框架 Apache Curator 中,包含了对 Zookeeper 分布式锁的实现。 github.com/apache/cura…

它的使用方式也很简单,如下所示:

这几种常见的“分布式锁”写法,搞懂再也不怕面试官,安排

 

我们看了下依然丝滑,源码我就不分析了,感兴趣的可以看我同事的博客 Curator的ZK分布式锁实现原理 。

总结

zookeeper 天生设计定位就是分布式协调,强一致性,锁很健壮。如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小。缺点: 在高请求高并发下,系统疯狂的加锁释放锁,最后 zk 承受不住这么大的压力可能会存在宕机的风险。

在这里简单的提一下,zk 锁性能比 redis 低的原因:zk 中的角色分为 leader,flower,每次写请求只能请求 leader,leader 会把写请求广播到所有 flower,如果 flower 都成功才会提交给 leader,其实这里相当于一个 2PC 的过程。在加锁的时候是一个写请求,当写请求很多时,zk 会有很大的压力,最后导致服务器响应很慢。

redis 锁实现简单,理解逻辑简单,性能好,可以支撑高并发的获取、释放锁操作。缺点: Redis 容易单点故障,集群部署,并不是强一致性的,锁的不够健壮; key 的过期时间设置多少不明确,只能根据实际情况调整;需要自己不断去尝试获取锁,比较消耗性能。

最后不管 redis 还是 zookeeper,它们都应满足分布式锁的特性:

  • 具备可重入特性(已经获得锁的线程在执行的过程中不需要再次获得锁)
  • 异常或者超时自动删除,避免死锁
  • 互斥(在分布式环境下同一时刻只能被单个线程获取)
  • 分布式环境下高性能、高可用、容错机制

各有千秋,具体业务场景具体使用。

作者:jack_xu

链接:https://juejin.im/post/5ee227b46fb9a047a86237cf

来源:掘金



Tags:分布式锁   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
概述以前参加过一个库存系统,由于其业务复杂性,搞了很多个应用来支撑。这样的话一份库存数据就有可能同时有多个应用来修改库存数据。比如说,有定时任务域xx.cron,和SystemA域...【详细内容】
2021-11-05  Tags: 分布式锁  点击:(31)  评论:(0)  加入收藏
作者 | mushishi来源 | urlify.cn/Mry6biredis分布式锁基本原理采用 redis 实现分布式锁,主要是利用其单线程命令执行的特性,一般是 setnx, 只会有一个线程会执行成功,也就是只...【详细内容】
2021-07-07  Tags: 分布式锁  点击:(105)  评论:(0)  加入收藏
前言之前的文章中通过电商场景中秒杀的例子和大家分享了单体架构中锁的使用方式,但是现在很多应用系统都是相当庞大的,很多应用系统都是微服务的架构体系,那么在这种跨jvm的场...【详细内容】
2021-01-05  Tags: 分布式锁  点击:(140)  评论:(0)  加入收藏
Nginx+Redis+MQ+DB下秒杀实现原理 Nginx+Redis+MQ+DB下限购实现原理 Nginx+Redis+MQ+DB下亿级流量实现原理 Redis在架构中的意义 分布式微服务是快了还是慢了 高可用和可用...【详细内容】
2020-12-22  Tags: 分布式锁  点击:(364)  评论:(0)  加入收藏
应用场景分布式系统中,面对高并发场景,又对数据一致性有一定要求的情况下,使用分布式锁。例如商城中下单扣库存这种情况。解决方案基于数据库例如:select * from mall_spu where...【详细内容】
2020-11-09  Tags: 分布式锁  点击:(174)  评论:(0)  加入收藏
当下在互联网技术架构中,最流行的莫过于分布式架构了。为什么大家纷纷都采用分布式架构呢? 1、高效低廉,将部署在高性能机的程序分散在多个小型机中部署;2、扩展性强,可随着业务...【详细内容】
2020-11-05  Tags: 分布式锁  点击:(96)  评论:(0)  加入收藏
分布式锁使用场景现在的系统都是集群部署,每个服务都不是单节点的了。比如库存服务,可能部署到3台机器上分别命名为节点1,节点2,节点3。库存服务需要扣减库存,扣减库存肯定需要锁...【详细内容】
2020-09-27  Tags: 分布式锁  点击:(140)  评论:(0)  加入收藏
在单进程的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量。...【详细内容】
2020-09-01  Tags: 分布式锁  点击:(44)  评论:(0)  加入收藏
分布式锁通常有很多选择,基于 Redis 的,基于 Zookeeper 的,基于数据库等等方案。Redis 用于缓存数据,在项目中都有使用,所以使用 Redis 来做分布式锁的会稍微多些。如果用 Redis...【详细内容】
2020-09-01  Tags: 分布式锁  点击:(102)  评论:(0)  加入收藏
分布式锁三种实现方式:1. 基于数据库实现分布式锁;2. 基于缓存(Redis等)实现分布式锁;3. 基于Zookeeper实现分布式锁; 一, 基于数据库实现分布式锁 1. 悲观锁利用select … w...【详细内容】
2020-07-30  Tags: 分布式锁  点击:(84)  评论:(0)  加入收藏
▌简易百科推荐
摘 要 (OF作品展示)OF之前介绍了用python实现数据可视化、数据分析及一些小项目,但基本都是后端的知识。想要做一个好看的可视化大屏,我们还要学一些前端的知识(vue),网上有很多比...【详细内容】
2021-12-27  项目与数据管理    Tags:Vue   点击:(1)  评论:(0)  加入收藏
程序是如何被执行的  程序是如何被执行的?许多开发者可能也没法回答这个问题,大多数人更注重的是如何编写程序,却不会太注意编写好的程序是如何被运行,这并不是一个好...【详细内容】
2021-12-23  IT学习日记    Tags:程序   点击:(9)  评论:(0)  加入收藏
阅读收获✔️1. 了解单点登录实现原理✔️2. 掌握快速使用xxl-sso接入单点登录功能一、早期的多系统登录解决方案 单系统登录解决方案的核心是cookie,cookie携带会话id在浏览器...【详细内容】
2021-12-23  程序yuan    Tags:单点登录(   点击:(8)  评论:(0)  加入收藏
下载Eclipse RCP IDE如果你电脑上还没有安装Eclipse,那么请到这里下载对应版本的软件进行安装。具体的安装步骤就不在这赘述了。创建第一个标准Eclipse RCP应用(总共分为六步)1...【详细内容】
2021-12-22  阿福ChrisYuan    Tags:RCP应用   点击:(7)  评论:(0)  加入收藏
今天想简单聊一聊 Token 的 Value Capture,就是币的价值问题。首先说明啊,这个话题包含的内容非常之光,Token 的经济学设计也可以包含诸多问题,所以几乎不可能把这个问题说的清...【详细内容】
2021-12-21  唐少华TSH    Tags:Token   点击:(9)  评论:(0)  加入收藏
实现效果:假如有10条数据,分组展示,默认在当前页面展示4个,点击换一批,从第5个开始继续展示,到最后一组,再重新返回到第一组 data() { return { qList: [], //处理后...【详细内容】
2021-12-17  Mason程    Tags:VUE   点击:(14)  评论:(0)  加入收藏
什么是性能调优?(what) 为什么需要性能调优?(why) 什么时候需要性能调优?(when) 什么地方需要性能调优?(where) 什么时候来进行性能调优?(who) 怎么样进行性能调优?(How) 硬件配...【详细内容】
2021-12-16  软件测试小p    Tags:性能调优   点击:(19)  评论:(0)  加入收藏
Tasker 是一款适用于 Android 设备的高级自动化应用,它可以通过脚本让重复性的操作自动运行,提高效率。 不知道从哪里听说的抖音 app 会导致 OLED 屏幕烧屏。于是就现学现卖,自...【详细内容】
2021-12-15  ITBang    Tags:抖音防烧屏   点击:(23)  评论:(0)  加入收藏
11 月 23 日,Rust Moderation Team(审核团队)在 GitHub 上发布了辞职公告,即刻生效。根据公告,审核团队集体辞职是为了抗议 Rust 核心团队(Core team)在执行社区行为准则和标准上...【详细内容】
2021-12-15  InfoQ    Tags:Rust   点击:(24)  评论:(0)  加入收藏
一个项目的大部分API,测试用例在参数和参数值等信息会有很多相似的地方。我们可以复制API,复制用例来快速生成,然后做细微调整既可以满足我们的测试需求1.复制API:在菜单发布单...【详细内容】
2021-12-14  AutoMeter    Tags:AutoMeter   点击:(20)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条