Redis技能—1.Redis分布式锁(这里介绍的方法居然是错误的)

使用过Redis分布式锁么,它是什么回事?(这里介绍的这两种方式其实在原子性方面都存在这漏洞:正确解法)

先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。

这时候对方会告诉你说你回答得不错,然后接着问如果在setnx之后执行expire之前进程意外crash或者要重启维护了,那会怎么样?

这时候你要给予惊讶的反馈:唉,是喔,这个锁就永远得不到释放了。紧接着你需要抓一抓自己得脑袋,故作思考片刻,好像接下来的结果是你主动思考出来的,然后回答:我记得set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!对方这时会显露笑容,心里开始默念:摁,这小子还不错。

背景:目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题。分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance),最多只能同时满足两项。”所以,很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。

一.redis命令讲解:
setnx()命令:setnx的含义就是SET if Not Exists,其主要有两个参数 setnx(key, value)。

该方法是原子的,如果key不存在,则设置当前key成功,返回1;如果当前key已经存在,则设置当前key失败,返回0。

 get()命令:get(key) 获取key的值,如果存在,则返回;如果不存在,则返回nil;

 getset()命令:  这个命令主要有两个参数 getset(key, newValue)。该方法是原子的,对key设置newValue这个值,并且返回key原来的旧值。 假设key原来是不存在的,那么多次执行这个命令,会出现下边的效果:

1. getset(key, "value1")  返回nil   此时key的值会被设置为value1

      2. getset(key, "value2")  返回value1   此时key的值会被设置为value2
      3. 依次类推!
二.具体的使用步骤如下:
     1. setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向2。
     2. get(lockkey)获取值oldExpireTime ,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向3。
     3. 计算newExpireTime=当前时间+过期超时时间,然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime。
     4. 判断currentExpireTime与oldExpireTime 是否相等,如果相等,说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。

     5. 在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行delete释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。

具体代码如下:

实现分布式锁DistributedLockHandler类:

[java]  view plain  copy
  1. package tk.mybatis.springboot.distributedLock;  
  2.   
  3. import org.springframework.stereotype.Service;  
  4.   
  5. import redis.clients.jedis.Jedis;  
  6.   
  7. @Service("distributedLockHandler")  
  8. public class DistributedLockHandler {  
  9.   
  10.     private static final Integer Lock_Timeout = 3;  
  11.   
  12.     private Jedis jedis;  
  13.   
  14.     /** 
  15.      * 外部调用加锁的方法 
  16.      * @param lockKey 锁的名字 
  17.      * @param timeout 超时时间(放置时间长度,如:5L) 
  18.      * @return 
  19.      */  
  20.     public boolean tryLock(String lockKey, Long timeout) {  
  21.         try {  
  22.             Long currentTime = System.currentTimeMillis();//开始加锁的时间  
  23.             boolean result = false;  
  24.               
  25.             while (true) {  
  26.                 if ((System.currentTimeMillis() - currentTime) / 1000 > timeout) {//当前时间超过了设定的超时时间  
  27.                     System.out.println("Execute DistributedLockHandler.tryLock method, Time out.");  
  28.                     break;  
  29.                 } else {  
  30.                     result = innerTryLock(lockKey);  
  31.                     if (result) {  
  32.                         break;  
  33.                     } else {  
  34.                         System.out.println("Try to get the Lock,and wait 100 millisecond....");  
  35.                         Thread.sleep(100);  
  36.                     }  
  37.                 }  
  38.             }  
  39.             return result;  
  40.         } catch (Exception e) {  
  41.             System.out.println("Failed to run DistributedLockHandler.getLock method."+ e);  
  42.             return false;  
  43.         }  
  44.     }  
  45.       
  46.     /** 
  47.      * 释放锁 
  48.      * @param lockKey 锁的名字 
  49.      */  
  50.     public void realseLock(String lockKey) {  
  51.         if(!checkIfLockTimeout(System.currentTimeMillis(), lockKey)){  
  52.             jedis.del(lockKey);  
  53.         }  
  54.     }  
  55.       
  56.     /** 
  57.      * 内部获取锁的实现方法 
  58.      * @param lockKey 锁的名字 
  59.      * @return 
  60.      */  
  61.     private boolean innerTryLock(String lockKey) {  
  62.           
  63.         long currentTime = System.currentTimeMillis();//当前时间  
  64.         String lockTimeDuration = String.valueOf(currentTime + Lock_Timeout + 1);//锁的持续时间  
  65.         Long result = jedis.setnx(lockKey, lockTimeDuration);  
  66.           
  67.         if (result == 1) {  
  68.             return true;  
  69.         } else {  
  70.             if (checkIfLockTimeout(currentTime, lockKey)) {  
  71.                 String preLockTimeDuration = jedis.getSet(lockKey, lockTimeDuration);  
  72.                 if (currentTime > Long.valueOf(preLockTimeDuration)) {  
  73.                     return true;  
  74.                 }  
  75.             }  
  76.             return false;  
  77.         }  
  78.           
  79.     }  
  80.   
  81.     /** 
  82.      * 判断加锁是否超时 
  83.      * @param currentTime 当前时间 
  84.      * @param lockKey 锁的名字 
  85.      * @return 
  86.      */  
  87.     private boolean checkIfLockTimeout(Long currentTime, String lockKey) {  
  88.         if (currentTime > Long.valueOf(jedis.get(lockKey))) {//当前时间超过锁的持续时间  
  89.             return true;  
  90.         } else {  
  91.             return false;  
  92.         }  
  93.     }  
  94.   
  95.     public DistributedLockHandler setJedis(Jedis jedis) {  
  96.         this.jedis = jedis;  
  97.         return this;  
  98.     }  
  99.   
  100. }  

调用Demo类:

[java]  view plain  copy
  1. package tk.mybatis.springboot.distributedLock;  
  2.   
  3. import redis.clients.jedis.Jedis;  
  4.   
  5. /** 
  6.  * 基于redis的setnx()、get()、getset()方法 分布式锁 
  7.  * @author KF01 
  8.  * 
  9.  */  
  10. public class Demo {  
  11.     private static final String lockKey = "Lock.TecentIm_Interface_Counter";  
  12.   
  13.     public static void main(String[] args) {  
  14.         Jedis jedis = new Jedis("127.0.0.1"6379);  
  15.           
  16.         DistributedLockHandler distributedLockHandler = new DistributedLockHandler().setJedis(jedis);  
  17.         try{  
  18.             boolean getLock = distributedLockHandler.tryLock(lockKey, Long.valueOf(5));  
  19.   
  20.             if(getLock){  
  21.                 // Do your job  
  22.                 System.out.println("Do your job........");  
  23.             }  
  24.   
  25.         }catch(Exception e){  
  26.             System.out.println(e);  
  27.         }finally {  
  28.             distributedLockHandler.realseLock(lockKey);  
  29.         }  
  30.           
  31.     }  
  32.       
  33.       
  34. }  

方案1,使用redis的setnx()、expire()方法,用于分布式锁:

      对于使用redis的setnx()、expire()来实现分布式锁,这个方案相对于memcached()的add()方案,redis占优势的是,其支持的数据类型更多,而memcached只支持String一种数据类型。除此之外,无论是从性能上来说,还是操作方便性来说,其实都没有太多的差异,完全看你的选择,比如公司中用哪个比较多,你就可以用哪个。

      首先说明一下setnx()命令,setnx的含义就是SET if Not Exists,其主要有两个参数 setnx(key, value)。该方法是原子的,如果key不存在,则设置当前key成功,返回1;如果当前key已经存在,则设置当前key失败,返回0。但是要注意的是setnx命令不能设置key的超时时间,只能通过expire()来对key设置。

      具体的使用步骤如下:

      1. setnx(lockkey, 1)  如果返回0,则说明占位失败;如果返回1,则说明占位成功

      2. expire()命令对lockkey设置超时时间,为的是避免死锁问题。

      3. 执行完业务代码后,可以通过delete命令删除key。

      这个方案其实是可以解决日常工作中的需求的,但从技术方案的探讨上来说,可能还有一些可以完善的地方。比如,如果在第一步setnx执行成功后,在expire()命令执行成功前,发生了宕机的现象,那么就依然会出现死锁的问题,所以如果要对其进行完善的话,可以使用redis的setnx()、get()和getset()方法来实现分布式锁。   

 

      方案2,使用redis的setnx()、get()、getset()方法,用于分布式锁:

      这个方案的背景主要是在setnx()和expire()的方案上针对可能存在的死锁问题,做了一版优化。

      那么先说明一下这三个命令,对于setnx()和get()这两个命令,相信不用再多说什么。那么getset()命令?这个命令主要有两个参数 getset(key,newValue)。该方法是原子的,对key设置newValue这个值,并且返回key原来的旧值。假设key原来是不存在的,那么多次执行这个命令,会出现下边的效果:

      1. getset(key, "value1")  返回nil   此时key的值会被设置为value1

      2. getset(key, "value2")  返回value1   此时key的值会被设置为value2

      3. 依次类推!

      介绍完要使用的命令后,具体的使用步骤如下:

      1. setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向2。

      2. get(lockkey)获取值oldExpireTime ,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向3。

      3. 计算newExpireTime=当前时间+过期超时时间,然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime。

      4. 判断currentExpireTime与oldExpireTime 是否相等,如果相等,说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。

      5. 在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行delete释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理。

      注意: 这个方案我当初在线上使用的时候是没有问题的,所以当初写这篇文章时也认为是没有问题的。但是截止到2017.05.13(周六),自己在重新回顾这篇文章时,我发现有两个问题比较集中:

      问题1:  在“get(lockkey)获取值oldExpireTime ”这个操作与“getset(lockkey, newExpireTime) ”这个操作之间,如果有N个线程在get操作获取到相同的oldExpireTime后,然后都去getset,会不会返回的newExpireTime都是一样的,都会是成功,进而都获取到锁???

      我认为这套方案是不存在这个问题的。依据有两条: 第一,redis是单进程单线程模式,串行执行命令。 第二,在串行执行的前提条件下,getset之后会比较返回的currentExpireTime与oldExpireTime 是否相等。

      问题2: 在“get(lockkey)获取值oldExpireTime ”这个操作与“getset(lockkey, newExpireTime) ”这个操作之间,如果有N个线程在get操作获取到相同的oldExpireTime后,然后都去getset,假设第1个线程获取锁成功,其他锁获取失败,但是获取锁失败的线程它发起的getset命令确实执行了,这样会不会造成第一个获取锁的线程设置的锁超时时间一直在延长???

      我认为这套方案确实存在这个问题的可能。但我个人认为这个微笑的误差是可以忽略的,不过技术方案上存在缺陷,大家可以自行抉择哈。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值