第二版,给锁增加了过期时间,避免出现死锁。但这两个命令不是原子的,第二步可能会失败,依然无法避免死锁问题。
127.0.0.1:6379> SETNX KEY_NAME VALUE
127.0.0.1:6379> EXPIRE KEY_NAME SECONDS
第三版,通过“SET…EX…NX”命令,将加锁、过期命令编排到一起,它们是原子操作了,可以避免死锁。
127.0.0.1:6379> SET KEY_NAME VALUE EX SECONDS NX
127.0.0.1:6379> SET job 10 EX 100 NX
2.2.1 出现的问题
看起来已经很完美了,但实际上还有隐患,如下图。
进程A在任务没有执行完毕时,锁已经到期被释放了。等进程A的任务执行结束后,它依然会尝试释放锁,因为它的代码逻辑就是任务结束后释放锁。但是,它的锁早已自动释放过了,它此时释放的可能是其他线程的锁。
2.2.2 原因
那么为什么会导致出现这种情况呢?
-
可能是我们评估操作共享资源的时间不准确导致的。出现了锁提前过期的状况。过期时间太短,那我们就增大锁过期的时间,例如设置过期时间为 20s,这样确实可以 [ 缓解 ] 这个问题,降低出问题的概率,但依旧 [ 无法彻底解决 ] 问题。因为进程A在拿到锁之后,在操作共享资源时,遇到的场景有可能是很复杂的,例如,程序内部发生异常、网络请求超时等等。
-
每个客户端在释放锁时,并没有检查这把锁是否还 [ 归自己持有 ] ,所以就会发生释放别人锁的风险。
2.2.3 解决方案
想要解决这些问题,我们需要解决两件事情:
- 在加锁时就要给锁设置一个 [ 唯一标识 ] ,进程要记住这个标识。当进程解锁的时候,要进行判断,是自己持有的锁才能释放,否则不能释放。可以为key赋一个随机值,来充当进程的标识。
127.0.0.1:6379> SET job $uuid EX 100 NX
- 解锁时要先判断,判断这把锁是不是自己持有的,然后再释放,这两步需要保证原子性,否则第二步失败的话,就会出现 [ 死锁 ] 。而获取和删除命令不是原子的,这就需要采用 [ Lua脚本 ] ,通过Lua脚本将两个命令编排在一起,而整个Lua脚本的执行是原子的。
因为 Redis 处理每一个请求是 [ 单线程 ] 执行的,在执行一个 Lua 脚本时,其它请求必须等待,直到这个 Lua 脚本处理完成,这样一来,GET + DEL 之间就不会插入其它命令了。
解锁
if redis.call(“get”, KEYS[1]) == ARGV[1]
then
return redis.call(“del”, KEYS[1])
else
return 0
end
==================================================================================
前面我们提到,锁的过期时间如果评估不好,这个锁就会有 [ 提前 ] 过期的风险。
当时给的妥协方案是,尽量 [ 冗余 ] 过期时间,降低锁提前过期的概率。
解决方案:加锁时,先设置一个过期时间,然后我们开启一个 [ 守护线程] ,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行 [ 续期] ,重新设置过期时间。
Redisson 是一个 Java 语言实现的 Redis SDK 客户端,在使用分布式锁时,它就采用了 [ 自动续期 ] 的方案来避免锁过期,这个守护线程我们一般也把它叫做 [ 看门狗 ] 线程。
3.1.1 小结
基于 Redis 的实现分布式锁,前面遇到的问题,以及对应的解决方案:
-
死锁:设置过期时间;
-
过期时间评估不好,锁提前过期:守护线程,自动续期;
-
锁被别人释放:锁写入唯一标识,释放锁先检查标识,再释放;
=====================================================================================
上述分布式锁的实现方案,是建立在单个主节点之上的。它的潜在问题如下:
进程A在主库上执行 SET 命令,加锁成功;此时,主库异常宕机,SET 命令还未同步到从库上(主从复制是异步的);从库被哨兵提升为新主库,这个锁在新的主库上发生了丢失了。
使用 Redlock(红锁)。
使用 Redlock 的方案基于 2 个前提:
-
不再需要部署从库和哨兵实例,只部署主库,不存在主从复制或者集群协调机制;
-
但主库要部署多个,官方推荐至少 5 个实例
也就是说,想用使用 Redlock,你至少要部署 5 个 Redis 实例,而且都是主库,它们之间没有任何关系,都是一个个孤立的实例。
-
客户端先获取 [ 当前时间戳Time1 ] ;
-
客户端依次向这 5 个 Redis 实例发起加锁请求(SET 命令),且每个请求会设置超时时间,如果某一个Redis加锁失败,就立即向下一个 Redis 实例申请加锁;
-
如果客户端从 >=3 个以上 Redis 实例加锁成功,则再次获取 [ 当前时间戳Time2 ],如果 Time2 - Time1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败;
-
加锁成功,去操作共享资源;
-
加锁失败,向 [ 全部节点 ] 发起释放锁请求(Lua 脚本释放锁);
客户端在多个 Redis 实例上申请加锁;必须保证大多数节点加锁成功;节点加锁的总耗时,要小于锁设置的过期时间;释放锁,要向全部节点发起释放锁请求。
4.1.1 Redlock 为什么这么做?
- 为什么要在多个实例上加锁?
本质上是为了容错,部分实例异常宕机,剩余的实例加锁成功,整个锁服务依旧可用。
- 为什么大多数加锁成功,才算成功?
多个 Redis 实例一起来用,其实就组成了一个分布式系统。在分布式系统中,总会出现异常节点,所以,在谈论分布式系统问题时,需要考虑异常节点达到多少个,也依旧不会影响整个系统的正确性。只要大多数节点正常,那么整个系统依旧是可以提供正确服务的。
- 为什么步骤 3 加锁成功后,还要计算加锁的累计耗时?
因为操作的是多个节点,所以耗时肯定会比操作单个实例耗时更久,而且,因为是网络请求,网络情况是复杂的,有可能存在延迟、丢包、超时等情况发生,网络请求越多,异常发生的概率就越大。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
总目录展示
该笔记共八个节点(由浅入深),分为三大模块。
高性能。 秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键。该笔记将从设计数据的动静分离方案、热点的发现与隔离、请求的削峰与分层过滤、服务端的极致优化这4个方面重点介绍。
一致性。 秒杀中商品减库存的实现方式同样关键。可想而知,有限数量的商品在同一时刻被很多倍的请求同时来减库存,减库存又分为“拍下减库存”“付款减库存”以及预扣等几种,在大并发更新的过程中都要保证数据的准确性,其难度可想而知。因此,将用一个节点来专门讲解如何设计秒杀减库存方案。
高可用。 虽然介绍了很多极致的优化思路,但现实中总难免出现一些我们考虑不到的情况,所以要保证系统的高可用和正确性,还要设计一个PlanB来兜底,以便在最坏情况发生时仍然能够从容应对。笔记的最后,将带你思考可以从哪些环节来设计兜底方案。
篇幅有限,无法一个模块一个模块详细的展示(这些要点都收集在了这份《高并发秒杀顶级教程》里),麻烦各位转发一下(可以帮助更多的人看到哟!)
由于内容太多,这里只截取部分的内容。
总难免出现一些我们考虑不到的情况,所以要保证系统的高可用和正确性,还要设计一个PlanB来兜底,以便在最坏情况发生时仍然能够从容应对。笔记的最后,将带你思考可以从哪些环节来设计兜底方案。
篇幅有限,无法一个模块一个模块详细的展示(这些要点都收集在了这份《高并发秒杀顶级教程》里),麻烦各位转发一下(可以帮助更多的人看到哟!)
[外链图片转存中…(img-dwIgAyC0-1712084047324)]
[外链图片转存中…(img-FnD0hyHB-1712084047325)]
由于内容太多,这里只截取部分的内容。