二Redis实现分布式锁

Posted zouxiangzhongyan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二Redis实现分布式锁相关的知识,希望对你有一定的参考价值。

前言:

  前面我们了解了什么是分布式锁,以及分布式锁使用的场景和分布式实现的原理,那么接下来我们就来看一下在实际开发中我们要向实现一个分布式锁,应该如何实现,常见的分布式所得实现方法有:基于数据库实现、基于缓存实现、基于zookeeper等,在这里我们来看一下基于缓存的分布式锁的实现。

一、版本一、基本实现:

  前面我们分析了我们要向实现一个分布式锁,要不许满足两个条件:

    多进程可见;

    互斥、锁的释放;

  1:多进程可见:

    Redis本身是基于jvm之外的第三方软件,而我们所有的服务器都会用到缓存,所以它天然的满足多进程可见这一条件;

  2:互斥:

    互斥就是说只能有一个进程获取到锁的标记,而这个我们可以基于Redis的setnx指令来实现,setnx指令的意思是,当多次执行该指令时,只有第一次执行的才会返回成功1,其他情况都会返回0,多个进程堆同一个key执行setnx指令,肯定只有一个能够执行成功,其他一定会失败,满足互斥的需求。

    技术图片

  3:释放锁:

    前面我们利用Redis的存储特性可以用来获取锁,那么我们获取到所执行完代码后就需要释放锁,释放锁我们则用到del命令,直接删除;但是这里存在另一个问题如果多个服务器其中一个服务器获取到了值,但是却宕机了,那么锁不久永远无法删除了吗?

    为了避免服务器宕机引起锁无法删除的问题,我们可以再获取锁的时候,给锁加一个有效期,当时间超出,就会自动释放,这样就付汇造成死锁了,但是setnx指令中并没有设置时间的功能,因此我们要借助set指令的NX和PX参数来实现。

      技术图片

    这其中指定下面几个参数:

      EX:过期时间,单位是秒;

      PX:过期时长,单位是毫秒;

      NX:等同于setnx;

      技术图片

     因此获取锁和释放锁的基本步骤如下:

      (1):通过set命令设置锁;

      (2):判断返回值是否OK;

        1):Nil,获取锁失败结束或者重试;

        2):OK,获取锁成功,执行业务,释放锁;

      (3):异常情况,服务器宕机,超时后会自动释放锁;

        技术图片

 

 二、版本二,互斥锁:

  上面我们分析并且实现了分布式锁,但是在上面的最初版本中却存在一个问题,我们思考一下,释放锁用DEL语句吧所删除,有没有这样一种可能;

    1:如果有三个进程A、B、C,如果此时A获取到了锁且设置超时时间是十秒;

    2:A开始执行任务由于某种原因业务阻塞了超过了十秒,此时把锁释放了;

    3:B恰好这个时候获取锁,由于所已经被A释放所以获取到了锁;

    4:A此时执行完了任务开始调用DEL删除所锁,于是把B的锁给释放了,而B此时还在执行任务;

    5:此时C尝试获取锁,也成功了,因为A把B的锁给删除了;

  问题出现了,B和C同时获取到了锁,违反了互斥的条件,那么如何解决这个问题呢,我们应该在删除锁之前,判断这个锁是不是属于自己的,如果不是就不要删除锁了,那么问题来了如何的值当前所是不是属于自己的?

  要想知道当前所是不是自己的,我们可以在set锁的时候,存入当前线程的唯一标识,删除锁前判断一下值是不是与自己的唯一标识一致,如果不一致就说明不是自己的锁,那就不要删除了。

  流程如图:

   技术图片

 三、版本三,重入锁:

  接下来我们来看一下分布式锁的第三个特性,如果我们在获取锁后,执行代码过程中再次尝试获取锁,执行setnx坑定会失败,这样就可能会导致死锁,这样的锁是重入的,那如何解决这一问题呢。

  我们都知道synchronized是一个重入锁,他的实现原理就是当现成执行synchronized代码时,会有一个参数记录当前执行的线程名称,当有重入锁出现时,会判断第二次拿锁的线程是否和第一次相同,如果相同则就放行。

  知道了synchronized重入锁的实现原理,那么在这里我们也可以借用synchronized实现重入锁的方式来实现分布式锁的重入锁;

  那么来看一下如何实现:

    1、获取锁:首次尝试获取锁,如果失败判断这个锁是否是自己的,如果是则允许再次获取锁,而且必须记录重复获取锁此时加1

    2、释放锁:在这里释放锁不能直接删除,因为锁是可重入的,如果锁进了多次在最内层删除了锁,导致外部的业务在没有锁的情况下执行,会有安全问题,因此必须获取锁重入的次数,释放锁时减去重入的次数,如果减到0,则可以删除。

  因此在存储锁的结构里必须包含的信息有,key、线程表示、锁的重如次数,不能在使用简单的key-value方式,在这里我们要使用hash接口的存储方式,key:hashKey:线程信息,hashValue:重如次数,默认次数为1

  分析了重入锁的实现来看一下实现重入锁的步骤,这其中需要用的一些Redis的命令,如下:

    1:EXISTS key:判断一个key是否存在;

    2:HEXISTS key field:判断一个hash的field是否存在;

    3:HSET key field value:给一个hash的field设置一个值;

    4:HINCRBY key field increment:给一个hash的field值增加指定的数值;

    5:EXPIRE key seconds:给一个key设置过期时间;

    6:DEL key:删除指定的key;

    获取锁步骤:

      1:判断lock是否存在,EXISTS lock;

        (1)、存在,说明有人获取到了锁,下面判断是不是自己;

          判断当前线程id作为hashKey是否存在,HEXISTS lock threadId;

            不存在,说明所已经有了,且不是自己获取的,获取锁失败,end;

            存在,说明是自己获取到的锁,重如次数+1,HINCRBY lock threadId 1;

         (2)、不存在,说明可以获取锁,HSET key threadId 1;

         (3)、设置锁自动释放时间,EXPIRE lock 20;

    释放锁步骤:

      1、判断当前线程id作为hashKey是否存在,HEXISTS lock threadId;

        不存在,说明锁已经失效,不用管;

        存在,说明锁还在,重如次数-1,HINCRBY lock threadId -1;

      2、判断重入次数是否为0:

        为0,则说明重入此时全部释放到了最外层,删除key,DEL lock;

        不为0,说明锁还在使用,重置有效时间,EXPIRE lock 20;

      技术图片

 

以上是关于二Redis实现分布式锁的主要内容,如果未能解决你的问题,请参考以下文章

Redis分布式锁的实现及注意事项

关于redis实现分布式锁

redis分布式锁的正确实现

Redis(十三):Redis分布式锁的正确实现方式

Redis分布式锁的正确实现方式

Redis分布式锁的正确实现方式