一步一步实现基于redis的分布式锁
Posted 打杂工程师
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一步一步实现基于redis的分布式锁相关的知识,希望对你有一定的参考价值。
前提
通过多线程请求一个接口,实现抢单的实现。
总数:10
线程数:200
测试方式:Jmeter
-
无锁状态
在无锁状态下,实现了库存的减少的业务逻辑。测试过程中,在没有并发情况下,redis中的库存数量按照理论逐渐递减。但是当使用Jmeter测试时,线程数量为200时,出现了库存数量重复的现象:
-
第一次优化——sychronized
考虑到在多线程情况下,会有重复抢单的问题,所以尝试使用JVM级锁来防止重复抢单。
通过使用synchronized加锁方式,似乎实现了库存的逐渐递减
但是,好像哪里不对……是的,我们当前测试的服务器,是一个service,如果在集群环境下,会是什么结果呢?我们通过再启动一个服务的方式,实现简单集群环境,测试结果如下:
我们发现在集群环境下,使用synchronized锁已经没有用了,所以现在需要考虑使用分布式锁了!
-
第二次优化——使用redis实现分布式锁
本次优化使用redis的SETNX来实现,具体原理就是: Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。
代码改动如下:
通过使用setnx的方式来代替synchronized的加锁,具体效果怎么样呢?这次我将库存设置成了50,然后再并发200的情况下测试,我们看一下测试的结果。
服务一的日志:
服务二的日志:
这样的结果,看上去满足了我们当时的要求了。
第三次优化:
但是回过头来在看一下代码,假如我们执行到34行以后,程序突然发生异常了呢?比如当前s-1操作发生异常或者其他复杂的业务逻辑异常。
没错,当我们程序在加锁以后发生异常,如果按照上述的代码,我们的redis锁,就永远不会释放了。
所以,我们需要假如try--finally了
这下可以了吗?
不,还是不行,因为同样的问题,假如程序在加锁之后发生非代码异常,比如服务器宕机、断电等特殊情况时,finally代码块同样没办法执行的。
第四次优化
如果遇到宕机、断电或者运维直接kill掉了服务的情况,我们就需要在加锁的时候,同时设置超时时间的方式来避免了:
到了这里,可能代码已经比较完美了,看起来好像也没有问题了,但是只有实践才是检验真理的唯一标准!
在这里,我就不频繁的测试了,我们之前项目中用的时候,就是这个样子,但是经过了实战的检验后,发现锁并没有很好的起作用。
第五次优化:
针对第四次优化,我们分析了好长时间,代码逻辑,加锁、超时时间、锁释放……都没有问题,但是为什么不起作用呢?
最后我们分析了一下高并发下的线程执行情况,发现:
如图所示,在高并发的情况下,每个请求线程其实耗时时间是不一定相同的,所以加入线程1耗时15秒,线程2耗时8秒,线程3耗时5秒……这下的话,当线程1还在执行完成业务逻辑时候,锁因为超时释放了,而线程2开始执行加锁操作,然后开始执行业务逻辑,此时线程1执行释放锁操作!!!这样的话,就会使得线程1把线程2加的锁释放了!!!然后线程3进入,同理线程2把线程3的锁释放了,这样下去,redis实现的分布式锁完全失效了!!!!OMG!!!太特么恐怖了!
如何解决呢?其实很简单,就是让每个线程只能释放它自己加的锁就可以了!
此时,redis分布式锁已经可以阻挡绝大部分的情况了,因为我们还有一个因素没有考虑——超时时间,这个时间其实很难把握,长了容易影响程序性能,短了容易造成锁失效,很无奈!
第六次优化——使用Redisson
使用redission框架,该框架中可以使用守护线程来不断的为当前线程续命——也就是每隔一段时间,就重新设置一次redis锁的超时时间,这样的话,锁就不会失效了,而且也不会太长了。
引入jar包(一定要注意引用jar包的版本,之前在网上随便找了一个版本,最后在启动项目的时候一直报错,很无解,试了半天换了一个版本就好了):
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.11.4</version>
</dependency>
添加Redisson配置信息:
package com.yang.api;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author:LCY
* @description:
* @date: create in 2019/10/18 17:32
* @Modified By:
*/
@Configuration
public class RedissionConfig
@Bean
public RedissonClient redisson()
Config config=new Config();
config.useClusterServers().setScanInterval(2000)
.setPassword("89db7ba4a")
.addNodeAddress("redis://172.24.161.234:7000")
.addNodeAddress("redis://172.24.161.234:7001")
.addNodeAddress("redis://172.24.161.234:7002");
单机模式 依次设置redis地址和密码
// config.useSingleServer().
// setAddress("redis://172.24.161.234:6379").
// setPassword("89db7ba4a");
return Redisson.create(config);
切记:在配置时,一定要将此配置文件放置到一个公共的项目中,配置一次即可。自己由于犯懒,直接在两个模拟相同的service中分别配置了一遍Redisson的配置信息,导致在程序运行的时候,发现分布式锁失效了。在网上查了半天,也没有一个说失效的例子,后来自己查看代码的时候,突然想到自己在项目中配置了两次,所以Redisson的锁是两把锁,所以根本不会起作用!!!
加锁代码:
总结
通过上面使用Redisson的方式加锁,基本上解决了分布式锁失败的情况,并且能够基本上应对大部分秒杀、高并发的场景。但是还有一个问题,那就是redis的集群模式中,可能会在redis主备切换的时候,导致锁失效!但是这种情况发生的概率已经是很低很低了,如果还需要考虑的话,其实redis已经没办法优化了,我们可以采取zookeeper的分布式锁来实现了。
通过以上的学习,发现所有的东西,都需要自己亲自去尝试一下才能有更多的收获。老师讲的再好,终究都是老师自己去实践的,而我们自己还是什么都不会。希望以后自己的学习都能够亲力亲为,这样虽然速度很慢,但是收获会非常丰硕!
最后希望自己能够心平气和做任何事情!也只有这样才能把事情做好!!
纯手工撰写,如果有什么不对的地方,欢迎各位大神指正,万分感谢!
文章测试代码连接:https://download.csdn.net/download/u010375663/11878890
以上是关于一步一步实现基于redis的分布式锁的主要内容,如果未能解决你的问题,请参考以下文章