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节点数,节省性能。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 上周热点回顾(2.17-2.23)
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· spring官宣接入deepseek,真的太香了~