Redis分布式锁

代码

背景:Redis中stock字段的value设为300

超卖问题:

多个请求(3个)同时调用这个接口,他们查出的stock都是300,都进行减1操作,实际上需要减3得到297,但都是执行300-1记录stock为299。

解决:使用jdk自带的锁,可以使多个请求排队,实现同一时间单个请求执行锁区间的代码。

架构背景:前端部署在nginx,实现负载均衡。请求实现分发到不同的服务器上。不同服务器,有着各自独立的jvm,jdk。

像synchronize,ReentrantLock是进程级别的,只能在当前进程内部有效。

问题:jvm提供的锁,集群情况下,这个是不能控制并发超卖问题的。

解决:使用Redis的setnx命令,实现分布式锁。

Redis执行请求命令时,是串行执行的。会进行排队。

setnx:有无存在此key,无则设置vlaue,有则不做操作。锁的效果,去处理并发资源问题。

代码实现:

多个请求同时调用接口,利用Redis接收all,实现一个请求setnx成功--锁住资源(商品),正常执行减商品逻辑,其他请求进行错误提示返回。

问题:执行后续减库存的请求,在后续逻辑都会有出现异常的可能,一旦过程出现异常,则导致后续无法执行释放锁,锁得不到释放,后续出现的请求无法正常拿到商品资源锁。

解决:加try,finally,释放锁放入finally。无论抛什么异常,都必须把锁释放掉。

问题:执行到一半,服务器宕机,不能执行到释放锁。

解决:加过期时间。

问题:执行完锁资源,服务器宕机了,锁得不到释放。

解决:将锁资源、设置过期时间设为原子操作,这样无论发生什么,只要设置锁,就会有锁释放的时候。

 

问题:锁过期后继续删锁操作,删成其他请求的锁。

第一个请求未删锁,锁过期时,删其他线程的锁。随后请求都变成了删后续请求的锁。

解决:加标识做为判断条件。设置uuid,将其放入锁的value,根据value值不同可以标识锁。在进行删锁操作时,先进行判断是否是自己上的锁。

问题:判断是自己的锁后,程序卡顿,这个请求的锁又提前过期了,后续恢复删锁,删的是他人的锁。

解决:改过期时间?但其实从根源上解决,过期时间整体是个固定值,不能动态适应程序执行过程所发生的未知。

动态:锁续命机制。

 Redisson

创建maven项目,添加依赖包

 <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.6.x</version>
 </dependency>

 初始化一个客户端,调用它的API,注入Bean容器里。

在使用的类中注入实例对象,通过实例对象得到锁对象,调用锁方法。

实现原理

底层代码

lockInterruptibly()方法:用于获取锁的方法,它属于Lock接口的一部分。与lock()方法不同,lockInterruptibly()方法可以响应中断。(对发出的中断操作做出响应。)

当一个线程正在等待获取锁时,如果其他线程调用了该线程的interrupt()方法,那么lockInterruptibly()方法会抛出InterruptedException,从而中断等待线程。

翻译:当前线程获取可中断锁,当其他线程使它阻塞时,线程就会选择中断操作。

 "Leasetime"可以指代网络租约期限、租用时间或锁的过期时间等。指锁的过期时间,即锁被持有的最长时间。如果超过这个时间,锁将被自动释放。

leaseTime为-1表示锁  没有设置租约时间限制,线程可以无限期持有该锁,直到被中断或自愿释放

tryAcquire()方法尝试获取锁

主线程:通过tryLockInnerAsync方法尝试异步获取锁,返回一个Future对象(异步计算结果的对象,允许你查询异步计算的状态)。

加锁异步执行的同时,会调用过期刷新方法

scheduleExpirationRenewal(threadId)方法内部部分逻辑。

执行了一个run方法,利用Lua判断主线程加的锁是否存在,还在则重新设置一个过期时间。

run方法末尾,写明run方法在主线程执行10s后执行。

 

同时run方法逻辑:异步执行计算方法,并监听此结果。

Future.getNow 是 Java 的 java.util.concurrent.Future 接口中的一个方法。它用于获取当前 Future 对象的结果,如果结果可用则立即返回,如果结果不可用或尚未计算完成,则返回一个默认值。

翻译:通过异步计算的方法得到的futrue对象是1(锁存在,设置过期时间)还是0(不存在)去看是否再次调用这个刷新方法自己本身(scheduleExpirationRenewal)。

 

找到异步执行的加锁方法,走异步加带有过期时间的锁的逻辑。

核心:

通过eval方法底层执行Lua脚本,通过if逻辑调用redis.call(执行命令)exists命令,判定key(后面getName方法传进来的商品id)是否存在。

then:设置key-value(线程id,相当于标识加了一把锁的clientid),再设置一个过期时间,底层有默认值。

线程id在分布式环境下有可能重复的,底层加了final UUID id;所以底层还是用了UUID做唯一标识。

底层设置的锁的默认时间:30s

Lua脚本

仍存在问题:

1、Redis主从节点切换过程中,锁丢失的问题,如何解决?

主节点加锁成功后,返回结果给线程1,线程1继续执行后面减库存逻辑。在同步从节点之前,挂了,这时从节点成为主节点,这时有线程请求,由于未同步锁状态,允许新的线程加锁并执行后续减库存逻辑。此时,出现并发操作同一资源问题。

2、setnx操作,相当于多个请求在排队,使之串行执行,并行改为串行执行,性能怎么优化

 

Redis(AP)只保证最终一致性,副本间的数据复制是异步进行的,主从切换后可能存在部分数据未复制过去的情况。而Zookeeper(CP)则更关注数据的一致性和同步。

Zookeeper更关注主从一致性,设置key后不是先返回给客户端,而是当同步结点超过三分之一才会返回客户端你写成功这个key了。如果leader节点崩了,底层会选举刚刚同步key的节点作为新leader结点。性能不如redis。 

Redlock底层和zookeeper类似。

给redis每个主节点加从结点,以此提高高可用性??不可。因为如果节点1还未同步给从节点就崩了,此时新请求来到新主节点加锁,加锁成功,往崩的主节点未同步的从节点加锁,也加锁成功。一个三个,服务端2加锁成功了,那客户端也加锁成功了相同的锁 ,那这俩节点就执行了相同的并发安全的代码,而且是针对同一个商品id。

redlock实现高可用不是加从节点,因为3个已经实现了高可用,当一个挂了,还有两个可以用来加锁,加锁成功了就能继续执行。所以加机器,但我们一般不推荐加很多机器,加多了操作redis影响性能的。一般3/5个,不用偶数个是因为4个能承受的也是一个挂,6个可承受的也是2个挂,与用3/5个一样,在达到效果一定的情况下,减少redis节点数,节省性能。

 

posted @   Anne起飞记  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· spring官宣接入deepseek,真的太香了~
点击右上角即可分享
微信分享提示