网站开发项目答辩主持词,域名网站负责人的责任,c 开发网站开发,网络推广和网络销售的区别作者 | 码哥字节来源 | 码哥字节Redis 分布式锁这个话题似乎烂大街了#xff0c;不管你是面试还是工作#xff0c;随处可见#xff0c;为啥还写#xff1f;因为看过很多文章没有将分布式锁的各种问题讲明白#xff0c;所以准备写一篇#xff0c;也当做自己的学习总结。在… 作者 | 码哥字节来源 | 码哥字节Redis 分布式锁这个话题似乎烂大街了不管你是面试还是工作随处可见为啥还写因为看过很多文章没有将分布式锁的各种问题讲明白所以准备写一篇也当做自己的学习总结。在进入正文之前我们先带着问题去思考什么时候需要分布式锁加、解锁的代码位置有讲究么如何避免出现死锁超时时间设置多少合适呢如何避免锁被其他线程释放如何实现重入锁主从架构会带来什么安全问题什么是 Redlock……分布式锁入门❝分布式锁应该满足哪些特性互斥在任何给定时刻只有一个客户端可以持有锁无死锁任何时刻都有可能获得锁即使获取锁的客户端崩溃容错只要大多数 Redis的节点都已经启动客户端就可以获取和释放锁。❝我可以使用 SETNX key value 命令是实现「互斥」特性。这个命令来自于SET if Not eXists的缩写意思是如果 key 不存在则设置 value 给这个key否则啥都不做。命令的返回值1设置成功0key 没有设置成功。如下场景敲代码一天累了想去放松按摩下肩颈。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❝码哥你见过「龙」么我见过因为我被一条龙服务过。肖彩机事情可没这么简单。这个方案存在一个存在造成锁无法释放的问题造成该问题的场景如下在按摩过程中突然收到线上报警提起裤子就跑去公司了没及时执行 DEL 释放锁客户端处理业务异常无法正确释放锁按摩过程中心肌梗塞嗝屁了无法执行 DEL指令。这样这个锁就会一直占用锁在我手里我挂了这样其他客户端再也拿不到这个锁了。异常导致没有删除锁❝我可以在获取锁成功的时候设置一个「超时时间」比如设定按摩服务一次 60 分钟那么在给这个 key 加锁的时候设置 60 分钟过期即可 SETNX lock:168 1 // 获取锁
(integer) 1EXPIRE lock:168 60 // 60s 自动删除
(integer) 1这样到点后锁自动释放其他客户就可以继续享受 168 技师按摩服务了。❝谁要这么写就糟透了。「加锁」、「设置超时」是两个命令他们不是原子操作。如果出现只执行了第一条第二条没机会执行就会出现「超时时间」设置失败依然出现死锁。❝那咋办我想被一条龙服务不能出现死锁啊。Redis 2.6.X 之后官方拓展了 SET 命令的参数满足了当 key 不存在则设置 value同时设置超时时间的语义并且满足原子性。SET resource_name random_value NX PX 30000NX表示只有 resource_name 不存在的时候才能 SET 成功从而保证只有一个客户端可以获得锁PX 30000表示这个锁有一个 30 秒自动过期时间。执行时间超过锁的过期时间❝这样我能稳妥的享受一条龙服务了么No还有一种场景会导致释放别人的锁客户 1 获取锁成功并设置设置 30 秒超时客户 1 因为一些原因导致执行很慢网络问题、发生 FullGC……过了 30 秒依然没执行完但是锁过期「自动释放了」客户 2 申请加锁成功客户 1 执行完成执行 DEL 释放锁指令这个时候就把 客户 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❝一路优化下来方案似乎比较「严谨」了抽象出对应的模型如下。通过 SET lock_resource_name $uuid NX PX $expire_time同时启动守护线程为快要过期单还没执行完毕的客户端的锁续命;客户端执行业务逻辑操作共享资源通过 Lua 脚本释放锁先 get 判断锁是否是自己加的再执行 DEL。加解锁代码位置有讲究根据前面的分析我们已经有了一个「相对严谨」的分布式锁了。于是「谢霸哥」就写了如下代码将分布式锁运用到项目中以下是伪代码逻辑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 则保存加锁的次数。通过 Lua 脚本实现原子性假设 KEYS1 「lock」, ARGV「1000uuid」---- 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 代表可重入次数被减 1null 代表其他线程尝试解锁解锁失败。主从架构带来的问题❝到这里分布式锁「很完美了」吧没想到分布式锁这么多门道。路还很远之前分析的场景都是锁在「单个」Redis 实例中可能产生的问题并没有涉及到 Redis 的部署架构细节。我们通常使用「Cluster 集群」或者「哨兵集群」的模式部署保证高可用。这两个模式都是基于「主从架构数据同步复制」实现的数据同步而 Redis 的主从复制默认是异步的。我们试想下如下场景会发生什么问题如果客户端 1 刚往 master 节点写入一个分布式锁此时这个指令还没来得及同步到 slave 节点。此时master 节点宕机其中一个 slave 被选举为新 master这时候新 master 是没有客户端 1 写入的锁锁丢失了。此刻客户端 2 线程来获取锁就成功了。虽然这个概率极低但是我们必须得承认这个风险的存在。❝Redis 的作者提出了一种解决方案叫 Redlock红锁Redis 的作者为了统一分布式锁的标准搞了一个 Redlock算是 Redis 官方对于实现分布式锁的指导规范https://redis.io/topics/distlock但是这个 Redlock 也被国外的一些分布式专家给喷了。因为它也不完美有“漏洞”。什么是 Redlock红锁是不是这个泡面吃多了你Redlock 红锁是为了解决主从架构中当出现主从切换导致锁丢失而提出的一种算法。大家可以看官方文档https://redis.io/topics/distlock以下来自官方文档的翻译。想用使用 Redlock官方建议部署 5 个 Redis 实例它们之间没有任何关系都是一个个孤立的实例。另外部署实例的数量要求是奇数。一个客户端要获取锁有 5 个步骤客户端获取当前时间 T1毫秒级别使用相同的 key和 value顺序尝试从 N个 Redis实例上获取锁。每个请求都设置一个超时时间毫秒级别该超时时间要远小于锁的有效时间这样便于快速尝试与下一个实例发送请求。比如锁的自动释放时间 10s则请求的超时时间可以设置 5~50 毫秒内这样可以防止客户端长时间阻塞。客户端获取当前时间 T2 并减去步骤 1 的 T1 来计算出获取锁所用的时间T3 T2 -T1。当且仅当客户端在大多数实例N/2 1获取成功且获取锁所用的总时间 T3 小于锁的有效时间才认为加锁成功否则加锁失败。如果第 3 步加锁成功则执行业务逻辑操作共享资源key 的真正有效时间等于有效时间减去获取锁所使用的时间步骤 3 计算的结果。如果因为某些原因获取锁失败没有在至少 N/21 个Redis实例取到锁或者取锁时间已经超过了有效时间客户端应该在所有的 Redis 实例上进行解锁即便某些 Redis 实例根本就没有加锁成功。❝事情可没这么简单Redis 作者把这个方案提出后受到了业界著名的分布式系统专家的质疑。两人好比神仙打架两人一来一回论据充足的对一个问题提出很多论断……由于篇幅原因关于 两人的争论分析以及 Redssion 对分布式锁的封装以及 Redlock 的实现我们下期再见。总结完工我建议你合上屏幕自己在脑子里重新过一遍每一步都在做什么为什么要做解决什么问题。我们一起从头到尾梳理了一遍 Redis分布式锁中的各种门道其实很多点是不管用什么做分布式锁都会存在的问题重要的是思考的过程。对于系统的设计每个人的出发点都不一样没有完美的架构没有普适的架构但是在完美和普适能平衡的很好的架构就是好的架构。往期推荐5G 落地进入爆发期是时候让毫米波登场了Github王炸功能Copilot替代打工人编程清华大学2021元宇宙研究报告Mendix 发布全球低代码报告点分享点收藏点点赞点在看