一步一步实现基于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的分布式锁的主要内容,如果未能解决你的问题,请参考以下文章

史上最细基于Redis实现的分布式Session解决单点登录问题,入门导师带你一步一步实现...

Java实现基于Redis的分布式锁

一步一步实现读写锁

redis锁的进化历程

基于redis实现分布式锁

基于redis的分布式锁