Redis 分布式锁没这么简单,网上大多数都有 bug

6a8ebc128a8ddccc392ea27161afe49d.gif

作者 | 码哥字节

来源 | 码哥字节

Redis 分布式锁这个话题似乎烂大街了,不管你是面试还是工作,随处可见,为啥还写?

因为看过很多文章没有将分布式锁的各种问题讲明白,所以准备写一篇,也当做自己的学习总结。

在进入正文之前,我们先带着问题去思考:

  • 什么时候需要分布式锁?

  • 加、解锁的代码位置有讲究么?

  • 如何避免出现死锁

  • 超时时间设置多少合适呢?

  • 如何避免锁被其他线程释放

  • 如何实现重入锁?

  • 主从架构会带来什么安全问题?

  • 什么是 Redlock

  • ……

分布式锁入门

分布式锁应该满足哪些特性?

  1. 互斥:在任何给定时刻,只有一个客户端可以持有锁;

  2. 无死锁:任何时刻都有可能获得锁,即使获取锁的客户端崩溃;

  3. 容错:只要大多数 Redis的节点都已经启动,客户端就可以获取和释放锁。

我可以使用 SETNX key value 命令是实现「互斥」特性。

这个命令来自于SET if Not eXists的缩写,意思是:如果 key 不存在,则设置 value 给这个key,否则啥都不做。

命令的返回值:

  • 1:设置成功;

  • 0:key 没有设置成功。

如下场景:

敲代码一天累了,想去放松按摩下肩颈。

168 号技师最抢手,大家喜欢点,所以并发量大,需要分布式锁控制。

同一时刻只允许一个「客户」预约 168 技师。

肖彩机申请 168 技师成功:

> SETNX lock:168 1
(integer) 1 # 获取 168 技师成功

谢霸哥后面到,申请失败:

> SETNX lock 2
(integer) 0 # 客户谢霸哥 2 获取失败

此刻,申请成功的客户就可以享受 168 技师的肩颈放松服务「共享资源」。

享受结束后,要及时释放锁,给后来者享受 168 技师的服务机会。

肖彩机,考考你如何释放锁呢?

很简单,使用 DEL 删除这个 key 就行。

> DEL lock:168
(integer) 1

码哥,你见过「龙」么?我见过,因为我被一条龙服务过。

053d91ccf859ac29113034b5372a04dd.png

肖彩机,事情可没这么简单。

这个方案存在一个存在造成锁无法释放的问题,造成该问题的场景如下:

  1. 在按摩过程中突然收到线上报警,提起裤子就跑去公司了,没及时执行 DEL 释放锁(客户端处理业务异常,无法正确释放锁);

  2. 按摩过程中心肌梗塞嗝屁了,无法执行 DEL指令。

这样,这个锁就会一直占用,锁在我手里,我挂了,这样其他客户端再也拿不到这个锁了。

异常导致没有删除锁

我可以在获取锁成功的时候设置一个「超时时间」

比如设定按摩服务一次 60 分钟,那么在给这个 key 加锁的时候设置 60 分钟过期即可:

> SETNX lock:168 1  // 获取锁
(integer) 1
> EXPIRE lock:168 60  // 60s 自动删除
(integer) 1

这样,到点后锁自动释放,其他客户就可以继续享受 168 技师按摩服务了。

谁要这么写,就糟透了。

「加锁」、「设置超时」是两个命令,他们不是原子操作。

如果出现只执行了第一条,第二条没机会执行就会出现「超时时间」设置失败,依然出现死锁。

那咋办,我想被一条龙服务,不能出现死锁啊。

Redis 2.6.X 之后,官方拓展了 SET 命令的参数,满足了当 key 不存在则设置 value,同时设置超时时间的语义,并且满足原子性。

SET resource_name random_value NX PX 30000
  • NX:表示只有 resource_name 不存在的时候才能 SET 成功,从而保证只有一个客户端可以获得锁;

  • PX 30000:表示这个锁有一个 30 秒自动过期时间。

执行时间超过锁的过期时间

这样我能稳妥的享受一条龙服务了么?

No,还有一种场景会导致释放别人的锁

  1. 客户 1 获取锁成功并设置设置 30 秒超时;

  2. 客户 1 因为一些原因导致执行很慢(网络问题、发生 FullGC……),过了 30 秒依然没执行完,但是锁过期「自动释放了」;

  3. 客户 2 申请加锁成功;

  4. 客户 1 执行完成,执行 DEL 释放锁指令,这个时候就把 客户 2 的锁给释放了。

有两个关键问题需要解决:

  1. 如何合理设置过期时间?

  2. 如何避免删除别人持有的锁。

正确设置锁超时

锁的超时时间怎么计算合适呢?

这个时间不能瞎写,一般要根据在测试环境多次测试,然后压测多轮之后,比如计算出平均执行时间 200 ms。

那么锁的超时时间就放大为平均执行时间的 3~5 倍。

为啥要放放大呢?

因为如果锁的操作逻辑中有网络 IO操作、JVM FullGC 等,线上的网络不会总一帆风顺,我们要给网络抖动留有缓冲时间。

那我设置更大一点,比如设置 1 小时不是更安全?

不要钻牛角,多大算大?

设置时间过长,一旦发生宕机重启,就意味着 1 小时内,分布式锁的服务全部节点不可用。

你要让运维手动删除这个锁么?

只要运维真的不会打你。

有没有完美的方案呢?不管时间怎么设置都不大合适。

我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁「续航」。

加锁的时候设置一个过期时间,同时客户端开启一个「守护线程」,定时去检测这个锁的失效时间。

如果快要过期,但是业务逻辑还没执行完成,自动对这个锁进行续期,重新设置过期时间。

这个道理行得通,可我写不出。

别慌,已经有一个库把这些工作都封装好了他叫Redisson

在使用分布式锁时,它就采用了「自动续期」的方案来避免锁过期,这个守护线程我们一般也把它叫做「看门狗」线程。

避免释放别人的锁

我要如何删除是自己加的锁呢?

出现释放别人锁的关键在于直接执行DEL指令,所以我们要想办法检查下这个锁是不是自己加的锁再执行删除指令。

解铃还须系铃人

我在加锁的时候设置一个「唯一标识」作为 value 代表加锁的客户端。

在释放锁的时候,客户端将自己的「唯一标识」比如可以使用随机数作为唯一标识,与锁上的「标识」比较是否相等,匹配上则删除,否则没有权利释放锁。

伪代码如下:

// 比对 value 与 唯一标识
if (redis.get("lock:168").equals(uuid)){redis.del("lock:168"); //比对成功则删除}

有没有想过,这是 GET + DEL 指令组合而成的,这里又会涉及到原子性问题。

我们可以通过 Lua 脚本来实现,这样判断和删除的过程就是原子操作了。

if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end

一路优化下来,方案似乎比较「严谨」了,抽象出对应的模型如下。

  1. 通过 SET lock_resource_name $uuid NX PX $expire_time,同时启动守护线程为快要过期单还没执行完毕的客户端的锁续命;

  2. 客户端执行业务逻辑操作共享资源;

  3. 通过 Lua 脚本释放锁,先 get 判断锁是否是自己加的,再执行 DEL

12c2b69d087851a36d7760a11e0aa78f.png

加解锁代码位置有讲究

根据前面的分析,我们已经有了一个「相对严谨」的分布式锁了。

于是「谢霸哥」就写了如下代码将分布式锁运用到项目中,以下是伪代码逻辑:

public void doSomething() {redisLock.lock(); // 上锁try {// 处理业务redisLock.unlock(); // 释放锁} catch (Exception e) {e.printStackTrace();}
}

一旦执行业务逻辑过程中抛出异常,程序就无法走下一步释放锁的流程。

所以释放锁的代码一定要放在 finally{} 块中。

加锁的位置也有问题,放在 try 外面的话,如果执行 redisLock.lock() 加锁异常,但是实际指令已经发送到服务端并执行,只是客户端读取响应超时,就会导致没有机会执行解锁的代码。

所以 redisLock.lock() 应该写在 try 代码块,这样保证一定会执行解锁逻辑。

综上所述,正确代码位置如下 :

public void doSomething() {// 上锁redisLock.lock();try {// 处理业务...} catch (Exception e) {e.printStackTrace();} finally {// 释放锁redisLock.unlock(); }
}

实现可重入锁

可重入锁要如何实现呢?重入之后,超时时间如何设置呢?

当一个线程执行一段代码成功获取锁之后,继续执行时,又遇到加锁的代码,可重入性就就保证线程能继续执行,而不可重入就是需要等待锁释放之后,再次获取锁成功,才能继续往下执行。

用一段代码解释可重入:

public synchronized void a() {b();
}
public synchronized void b() {// pass
}

假设 X 线程在 a 方法获取锁之后,继续执行 b 方法,如果此时不可重入,线程就必须等待锁释放,再次争抢锁。

锁明明是被 X 线程拥有,却还需要等待自己释放锁,然后再去抢锁,这看起来就很奇怪,我释放我自己~

Redis Hash 可重入锁

Redisson 类库就是通过 Redis Hash 来实现可重入锁,未来码哥会专门写一篇关于 Redisson 的使用与原理的文章……

当线程拥有锁之后,往后再遇到加锁方法,直接将加锁次数加 1,然后再执行方法逻辑。

退出加锁方法之后,加锁次数再减 1,当加锁次数为 0 时,锁才被真正的释放。

可以看到可重入锁最大特性就是计数,计算加锁的次数。

所以当可重入锁需要在分布式环境实现时,我们也就需要统计加锁次数。

加锁逻辑

我们可以使用 Redis hash 结构实现,key 表示被锁的共享资源, hash 结构的 fieldKey 的 value 则保存加锁的次数。

2c72d788888532bf8396e9b95753ca77.png

通过 Lua 脚本实现原子性,假设 KEYS1 = 「lock」, ARGV「1000,uuid」:

---- 1 代表 true
---- 0 代表 falseif (redis.call('exists', KEYS[1]) == 0) thenredis.call('hincrby', KEYS[1], ARGV[2], 1);redis.call('pexpire', KEYS[1], ARGV[1]);return 1;
end ;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) thenredis.call('hincrby', KEYS[1], ARGV[2], 1);redis.call('pexpire', KEYS[1], ARGV[1]);return 1;
end ;
return 0;

加锁代码首先使用 Redis exists 命令判断当前 lock 这个锁是否存在。

如果锁不存在的话,直接使用 hincrby创建一个键为 lock hash 表,并且为 Hash 表中键为 uuid 初始化为 0,然后再次加 1,最后再设置过期时间。

如果当前锁存在,则使用 hexists判断当前 lock 对应的 hash 表中是否存在 uuid 这个键,如果存在,再次使用 hincrby 加 1,最后再次设置过期时间。

最后如果上述两个逻辑都不符合,直接返回。

解锁逻辑

-- 判断 hash set 可重入 key 的值是否等于 0
-- 如果为 0 代表 该可重入 key 不存在
if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) thenreturn nil;
end ;
-- 计算当前可重入次数
local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1);
-- 小于等于 0 代表可以解锁
if (counter > 0) thenreturn 0;
elseredis.call('del', KEYS[1]);return 1;
end ;
return nil;

首先使用 hexists 判断 Redis Hash 表是否存给定的域。

如果 lock 对应 Hash 表不存在,或者 Hash 表不存在 uuid 这个 key,直接返回 nil

若存在的情况下,代表当前锁被其持有,首先使用 hincrby使可重入次数减 1 ,然后判断计算之后可重入次数,若小于等于 0,则使用 del 删除这把锁。

解锁代码执行方式与加锁类似,只不过解锁的执行结果返回类型使用 Long。这里之所以没有跟加锁一样使用 Boolean ,这是因为解锁 lua 脚本中,三个返回值含义如下:

  • 1 代表解锁成功,锁被释放

  • 0 代表可重入次数被减 1

  • null 代表其他线程尝试解锁,解锁失败。

主从架构带来的问题

到这里分布式锁「很完美了」吧,没想到分布式锁这么多门道。

路还很远,之前分析的场景都是,锁在「单个」Redis 实例中可能产生的问题,并没有涉及到 Redis 的部署架构细节。

我们通常使用「Cluster 集群」或者「哨兵集群」的模式部署保证高可用。

这两个模式都是基于「主从架构数据同步复制」实现的数据同步,而 Redis 的主从复制默认是异步的。

我们试想下如下场景会发生什么问题:

  1. 如果客户端 1 刚往 master 节点写入一个分布式锁,此时这个指令还没来得及同步到 slave 节点。

  2. 此时,master 节点宕机,其中一个 slave 被选举为新 master,这时候新 master 是没有客户端 1 写入的锁,锁丢失了。

  3. 此刻,客户端 2 线程来获取锁,就成功了。

虽然这个概率极低,但是我们必须得承认这个风险的存在。

Redis 的作者提出了一种解决方案,叫 Redlock(红锁)

Redis 的作者为了统一分布式锁的标准,搞了一个 Redlock,算是 Redis 官方对于实现分布式锁的指导规范,https://redis.io/topics/distlock,但是这个 Redlock 也被国外的一些分布式专家给喷了。

因为它也不完美,有“漏洞”。

什么是 Redlock

红锁是不是这个?

66927c1e8b11ed003bc02366bfabc85a.png

泡面吃多了你,Redlock 红锁是为了解决主从架构中当出现主从切换导致锁丢失而提出的一种算法。

大家可以看官方文档(https://redis.io/topics/distlock),以下来自官方文档的翻译。

想用使用 Redlock,官方建议部署 5 个 Redis 实例,它们之间没有任何关系,都是一个个孤立的实例

另外部署实例的数量要求是奇数。

一个客户端要获取锁有 5 个步骤

  1. 客户端获取当前时间 T1(毫秒级别);

  2. 使用相同的 keyvalue顺序尝试从 NRedis实例上获取锁。

  • 每个请求都设置一个超时时间(毫秒级别),该超时时间要远小于锁的有效时间,这样便于快速尝试与下一个实例发送请求。

  • 比如锁的自动释放时间 10s,则请求的超时时间可以设置 5~50 毫秒内,这样可以防止客户端长时间阻塞。

客户端获取当前时间 T2 并减去步骤 1 的 T1 来计算出获取锁所用的时间(T3 = T2 -T1)。当且仅当客户端在大多数实例(N/2 + 1)获取成功,且获取锁所用的总时间 T3 小于锁的有效时间,才认为加锁成功,否则加锁失败。

如果第 3 步加锁成功,则执行业务逻辑操作共享资源,key 的真正有效时间等于有效时间减去获取锁所使用的时间(步骤 3 计算的结果)。

如果因为某些原因,获取锁失败(没有在至少 N/2+1 个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的 Redis 实例上进行解锁(即便某些 Redis 实例根本就没有加锁成功)。

事情可没这么简单,Redis 作者把这个方案提出后,受到了业界著名的分布式系统专家的质疑

两人好比神仙打架,两人一来一回论据充足的对一个问题提出很多论断……

由于篇幅原因,关于 两人的争论分析以及 Redssion 对分布式锁的封装以及 Redlock 的实现我们下期再见。

总结

完工,我建议你合上屏幕,自己在脑子里重新过一遍,每一步都在做什么,为什么要做,解决什么问题。

我们一起从头到尾梳理了一遍 Redis分布式锁中的各种门道,其实很多点是不管用什么做分布式锁都会存在的问题,重要的是思考的过程。

对于系统的设计,每个人的出发点都不一样,没有完美的架构,没有普适的架构,但是在完美和普适能平衡的很好的架构,就是好的架构。

9979be63c545ed5622dcc99d7260b289.gif

44aea2439abcf169fa0c8a9c566d83d5.png

往期推荐

5G 落地进入爆发期,是时候让毫米波登场了

Github王炸功能!Copilot替代打工人编程?

清华大学:2021元宇宙研究报告

Mendix 发布全球低代码报告

6b4a896e31410451de49c09c06f80c21.gif

点分享

b4dca9d86a4bd2b7c1057225d7354eb5.gif

点收藏

82e427ca9102346622ff5b07ce684d36.gif

点点赞

26295fcdcd873508bd8c1ff139d3ef88.gif

点在看

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/513364.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python多线程同步与互斥_python多线程编程(3): 使用互斥锁同步线程

问题的提出上一节的例子中,每个线程互相独立,相互之间没有任何关系。现在假设这样一个例子:有一个全局的计数num,每个线程获取这个全局的计数,根据num进行一些处理,然后将num加1。很容易写出这样的代码&…

php 商品展示html,HTML5和CSS3实现3D展示商品信息的代码

这篇文章介绍的是关于html5商品展示导购特效是一款3D立体样式的商品信息,非常具有实用价值,需要的朋友可以参考下。强化下perspective和transform:translateZ的用法。传统的商品展示或许并不能很好的吸引用户的注意力,但是如果在展示中添加适…

asp.net程序涉及案例_定制小程序 | 企业在开发小程序前需要满足哪些条件?

目前深圳小程序开发行业发展愈发迅速,小程序开发已成为目前的潮流趋势,制作属于自己的小程序刻不容缓。那么在开发小程序前,企业需要满足什么条件呢?1、注册账号万纵联小编告诉你,要想开发小程序,申请账号是…

封神-核心功能 | 钉钉告警+数据网关

简介: 封神-核心功能 | 钉钉告警数据网关1. 开发背景 1.1 用户痛点 ①租户侧运维能力弱 问题:租户侧,客户没有有效途径,及时地获取实例级的状态、性能、容量的数据。 现状:每日固定时间,驻场需要人肉收集数…

usleep延时0.毫秒_【进阶】用swoole实现订单的延时处理(自动取消,还原库存等)...

文章正文一、业务场景:当客户下单在指定的时间内如果没有付款,那我们需要将这笔订单取消掉,比如好的处理方法是运用延时取消,很多人首先想到的当然是crontab,这个也行,不过这里我们运用swoole的异步毫秒定时…

boa php5.5 移植,BOA+PHP在Tiny6410上的移植

1、下载php-5.2.17.tar.bz22、解压到/vptemp#tar -jxv -f php-5.2.17.tar.bz2 -C /vptemp3、编译# cd /vptemp/php-5.2.17#CCarm-linux-gcc ./configure --hostarm-linux --prefix/opt/php5 --disable-all --enable-pdo --with-sqlite3 --with-sqlite --with-pdo-sqlite --with…

中国电信天翼云进入4.0阶段,打造一朵无处不在的分布式云

11月12日,天翼云在2021国际数字科技展暨天翼智能生态博览会天翼云论坛上,发布并解读了全新升级的天翼云4.0分布式云,同时还推出了以红色为主色调的天翼云全新品牌形象。 天翼云新品牌形象 中国电信集团有限公司副总经理唐珂在致辞中表示&am…

软件开发中 前台、中台、后台英文_最近处处惹人爱的中台到底是什么

在当下互联网圈子里要问什么最火莫过于中台这一概念了,各大公司都开始了一轮跑马圈地似的中台建设,那么到底中台是什么呢?本文我们就来谈谈这个话题。一、什么是前台,后台在以往的互联网企业生产流程中,我们可以将研发…

如何基于MaxCompute快速打通数据仓库和数据湖的湖仓一体实践

简介: MaxCompute 是面向分析的企业级 SaaS 模式云数据仓库,以 Serverless 架构提供快速、全托管的在线数据仓库服务,消除了传统数据平台在资源扩展性和弹性方面的限制,最小化用户运维投入,使您可以经济并高效的分析处…

数仓架构的持续演进与发展 — 云原生、湖仓一体、离线实时一体、SaaS模式

简介: 数据仓库概念从1990年提出,经过了四个主要阶段。从最初的数据库演进到数据仓库,到MPP架构,到大数据时代的数据仓库,再到今天的云原生的数据仓库。在不断的演进过程中,数据仓库面临着不同的挑战。 作…

华为路由器命令手册_华为路由器+蒲公英路由器,如何做双层路由器映射?

今天上午,有一个客户反馈,他们的软件和linux系统无法实现外网访问了。客户的网络拓扑是这样的,光猫接蒲公英路由器X5,蒲公英路由器的LAN 口IP是192.168.0.1,然后从蒲公英路由器的LAN口连接一根网线到华为路由器WAN口&a…

行业务实派:解锁数据价值,翼方健数全栈隐私安全计算技术

数智化时代,数据已成为最核心、最具价值的生产要素。其中,如何应用可落地的技术,共享和保护数据,促进数据要素进一步流通,释放数据价值,受到业内普遍关注。 隐私计算,被业内誉为打破“数据隐私…

css中分区显示php,CSS中7个你必须知道属性

学习CSS是构建好看网页的一种方式。 但是,在学习过程中,我们倾向于(大部分时间)限制自己,一遍又一遍地使用相同的属性。 毕竟,我们是一种习惯性的动物,我们会使用自己习惯且熟悉的东西。因此,在这篇文章中&…

ctrl c 失效了_[安卓+PC双端]超C女仆无馬中字

这周基本啥也没发,传个小游戏吧就~点赞(150) 投币(随缘)推荐用用吉里吉里模拟器玩~应该是女仆类的游戏,我没玩过也不清楚哦链接失效我就不补了,如果点赞在今天内达标,那就今晚20:00补发,不要错…

浅谈分布式一致性:Raft 与 SOFAJRaft

简介: SOFAJRaft已开源 作者 | 家纯 来源 | 阿里技术公众号 一 分布式共识算法 (Consensus Algorithm) 1 如何理解分布式共识? 多个参与者针对某一件事达成完全一致:一件事,一个结论。 已达成一致的结论,不可推翻。 2 有哪些…

OpenKruise v0.9.0 版本发布:新增 Pod 重启、删除防护等重磅功能

简介: OpenKruise 是阿里云开源的云原生应用自动化管理套件,也是当前托管在 Cloud Native Computing Foundation (CNCF) 下的 Sandbox 项目。它来自阿里巴巴多年来容器化、云原生的技术沉淀,是阿里内部生产环境大规模应用的基于 Kubernetes 之…

00后确实卷,公司新来的卷王,我们这帮老油条真干不过.....

都说00后躺平了,但是有一说一,该卷的还是卷。这不,前段时间我们公司来了个00后,工作没两年,跳槽到我们公司起薪18K,都快接近我了。后来才知道人家是个卷王,从早干到晚就差搬张床到工位睡觉了。最…

rsa 返回值 验签 失败_解析蛋糕、面包制作失败的原因

其实这些问题的答案大多都隐藏在搅拌盘、烤炉和烤盘中掌握好这些基础知识就能告别这些常见的失误01﹏﹏﹏好的蛋糕始于搅拌盘做蛋糕的过程实质上是化学变化的过程,一系列的配料遵照严格的顺序倒入搅拌盘里混合并产生特殊效应。想做出蛋糕心松软、湿润和细腻质感的黄…

日期 java cal,日期系列教材 (三)- 如何使用Java的Calendar类

代码比较复制代码package date;import java.text.SimpleDateFormat;//import java.util.Calendar;import java.util.Date;public class TestDate {private static SimpleDateFormat sdf new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static void main(Strin…

阿里云资深技术专家李克畅谈边缘云计算趋势与实践

简介: 2021年5月15日,以“相信边缘的力量”为主题的全球边缘计算大会在深圳成功召开。 阿里云资深技术专家李克,分享阿里云在边缘云计算的探索和实践,如何为行业提供广覆盖、低成本、高可靠的边缘基础设施。 2021年5月15日&#…