Redis实现分布式锁相关注意事项 
查看了不少关于redis实现分布式锁的文章,无疑要设计一个靠谱的分布式并不太容易,总会出现各种鬼畜的问题;现在就来小述一下,在设计一个分布式锁的过程中,会遇到一些什么问题
 
I. 背景知识 借助redis来实现分布式锁(我们先考虑单机redis的模式),首先有必要了解下以下几点:
单线程模式 
setnx  : 当不存在时,设置value,并返回1; 否则返回0 
getset : 设置并获取原来的值 
expire : 设置失效时间 
get    : 获取对应的值 
del    : 删除 
ttl    : 获取key对应的剩余时间,若key没有设置过超时时间,或者压根没有这个key则返回负数(可能是-1,-2) 
watch/unwatch : 事务相关 
 
II. 方案设计 1. 设计思路 获取锁: 
调用 setnx 尝试获取锁,如果设置成功,表示获取到了锁 
设置失败,此时需要判断锁是否过期
未过期,则表示获取失败;循环等待,并再次尝试获取锁 
已过期,getset再次设置锁,判断是否获取了锁(根据返回的值进行判断,后面给出具体的方案) 
若失败,则重新进入获取锁的逻辑 
 
 
 
释放锁: 
2. getset的实现方案 网上一种常见的case,主要思路如下
setnx 尝试获取锁 
失败,则 get 获取锁的value (一般是 uuid_timstamp) 
判断是否过期,若没有过期,则表示真的获取失败 
若过期,则采用 getset设置,尝试获取锁 
 
实现代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public  class  DistributeLock      private  static  final  Long OUT_TIME = 30L ;     public  String tryLock (Jedis jedis, String key)           while  (true ) {             String value = UUID.randomUUID().toString() + "_"  + System.currentTimeMillis();             Long ans = jedis.setnx(key, value);             if  (ans != null  && ans == 1 ) {                  return  value;             }                          String oldLock = jedis.get(key);             if  (oldLock == null ) {                 continue ;             }             long  oldTime = Long.parseLong(oldLock.substring(oldLock.lastIndexOf("_" ) + 1 ));             long  now = System.currentTimeMillis();             if  (now - oldTime < OUT_TIME) {                  continue ;             }             String getsetOldVal = jedis.getSet(key, value);             if  (Objects.equals(oldLock, getsetOldVal)) {                  return  value;             } else  {                  jedis.set(key, getsetOldVal);             }         }     }     public  void  tryUnLock (Jedis jedis, String key, String uuid)           String ov = jedis.get(key);         if  (uuid.equals(ov)) {              jedis.del(key);         }     } } 
观察获取锁的逻辑,特别是获取超时锁的逻辑,很容易想到有一个问题 getSet 方法会不会导致写数据混乱的问题,简单来说就是多个线程同时判断锁超时时,执行 getSet设置锁时,最终获取锁的线程,能否保证和redis中的锁的value相同
上面的实现方式,一个混乱的case如下:
三个线程a,b,c 都进入到了锁超时的阶段 
线程a, 获取原始值 oldVal, 并设置 t1 
线程b, 获取线程a设置的 t1, 并重设为 t2 
线程c, 获取线程b设置的 t2, 并重设为 t3 
线程a,判断,并正式获取到锁 
线程b,判断失败,恢复原来锁的内容为t1 
线程c, 判断失败,恢复原来锁的内容为t2 
问题出现了,获取锁的线程a,期望所得内容为t1, 但是实际为t2; 导致无法释放锁 
 
实际验证
在上面的代码中,配合测试case,加上一些日志输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public  static  String tryLock (Jedis jedis, String key)  throws  InterruptedException     String threadName = Thread.currentThread().getName();     while  (true ) {         String value = threadName + "_"  + UUID.randomUUID().toString() + "_"  + System.currentTimeMillis();         Long ans = jedis.setnx(key, value);         if  (ans != null  && ans == 1 ) {              return  value;         }                  String oldLock = jedis.get(key);         if  (oldLock == null ) {             continue ;         }         long  oldTime = Long.parseLong(oldLock.substring(oldLock.lastIndexOf("_" ) + 1 ));         long  now = System.currentTimeMillis();         if  (now - oldTime < OUT_TIME) {              continue ;         }                  Thread.sleep(50 );         System.out.println(threadName + " in getSet!" );                  if  ("t2" .equalsIgnoreCase(threadName)) {             Thread.sleep(20 );         } else  if  ("t3" .equalsIgnoreCase(threadName)) {             Thread.sleep(40 );         }         String getsetOldVal = jedis.getSet(key, value);         System.out.println(threadName + " set redis value: "  + value);         if  (Objects.equals(oldLock, getsetOldVal)) {              System.out.println(threadName + " get lock!" );             if  ("t1" .equalsIgnoreCase(threadName)) {                                  Thread.sleep(40 );             }             return  value;         } else  {                           if  ("t3" .equalsIgnoreCase(threadName)) {                 Thread.sleep(40 );             } else  if  ("t2" .equalsIgnoreCase(threadName)){                 Thread.sleep(20 );             }             jedis.set(key, getsetOldVal);             System.out.println(threadName + " recover redis value: "  + getsetOldVal);         }     } } 
测试case
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @Test public  void  testLock ()  throws  InterruptedException          JedisPool jedisPool = cacheWrapper.getJedisPool(0 );     Jedis jedis = jedisPool.getResource();     String lockKey = "lock_test" ;     String old = DistributeLock.tryLock(jedis, lockKey);     System.out.println("old lock: "  + old);          Thread.sleep(40 );          Thread t1 = new  Thread(() -> {         try  {             Jedis j =jedisPool.getResource();             DistributeLock.tryLock(j, lockKey);             System.out.println("t1 >>>> "  + j.get(lockKey));         } catch  (InterruptedException e) {             e.printStackTrace();         }     }, "t1" );     Thread t2 = new  Thread(() -> {         try  {             Jedis j =jedisPool.getResource();             DistributeLock.tryLock(j, lockKey);             System.out.println("t2 >>>>> "  + j.get(lockKey));         } catch  (InterruptedException e) {             e.printStackTrace();         }     }, "t2" );     Thread t3 = new  Thread(() -> {         try  {             Jedis j =jedisPool.getResource();             DistributeLock.tryLock(j, lockKey);             System.out.println("t3 >>>>> "  + j.get(lockKey));         } catch  (InterruptedException e) {             e.printStackTrace();         }     }, "t3" );     t1.start();     t2.start();     t3.start();     Thread.sleep(10000 ); }; 
部分输出结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 main in  getSet! main set  redis value: main_d4cc5d69-5027-4550-abe1-10126f057779_1515643763130 main get lock! old lock: main_d4cc5d69-5027-4550-abe1-10126f057779_1515643763130 t1 in  getSet! t2 in  getSet! t1 set  redis value: t1_105974db-7d89-48bf-9669-6f122a3f9fb6_1515643763341 t1 get lock! t3 in  getSet! t2 set  redis value: t2_be06f80a-9b70-4a0e-a86d-44337abe8642_1515643763341 t1 >>>> t2_be06f80a-9b70-4a0e-a86d-44337abe8642_1515643763341 t3 set  redis value: t3_9aa5d755-43b2-43bd-9a0b-2bad13fa31f6_1515643763345 t2 recover redis value: t1_105974db-7d89-48bf-9669-6f122a3f9fb6_1515643763341 t3 recover redis value: t2_be06f80a-9b70-4a0e-a86d-44337abe8642_1515643763341 
重点关注 t1 >>>> t2_be06f80a-9b70-4a0e-a86d-44337abe8642_1515643763341,表示t1线程过去了锁,但是锁的内容不是其value,即便t2去恢复,也会被t3给覆盖
如何解决上面这个问题呢? 
上面是典型的并发导致的问题,当然可以考虑从解决并发问题的角度出发来考虑,一个常见的方式就是加锁了,思路如下:(不详细展开了)
在判断超时之后,加锁 
再次获取对应的值,判断是否超时,是则执行上面的操作 
否则退出逻辑,继续循环 
 
这种实现方式,会有以下的问题: 
getset 这个方法执行,可能导致写入脏数据 
基于服务器时钟进行超时判断,要求所有服务器始终一致,否则有坑 
 
3. expire实现方式 相比于前面一种直接将value设置为时间戳,然后来比对的方法,这里则直接借助redis本身的expire方式来实现超时设置,主要实现逻辑相差无几
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public  class  DistributeExpireLock      private  static  final  Integer OUT_TIME = 3 ;     public  static  String tryLock (Jedis jedis, String key)           String value = UUID.randomUUID().toString();         while (true ) {             Long ans = jedis.setnx(key, value);             if  (ans != null  && ans == 1 ) {                  jedis.expire(key, OUT_TIME);                  return  value;             }                                       if (jedis.ttl(key) < 0 ) {                 jedis.expire(key, OUT_TIME);             }         }     }     public  static  void  tryUnLock (Jedis jedis, String key, String uuid)           String ov = jedis.get(key);         if  (uuid.equals(ov)) {              jedis.del(key);             System.out.println(Thread.currentThread() +" del lock success!" );         } else  {             System.out.println(Thread.currentThread() +" del lock fail!" );         }     } } 
获取锁的逻辑相比之前的,就简单很多了,接下来则需要简单的分析下,上面这种实现方式,会不会有坑呢?我们主要看一下获取锁失败的场景
如果获取锁失败 
表示有其他的业务方已经获取到了锁 
此时,只能等持有锁的业务方主动释放锁 
判断锁是否设置了超时时间,若没有则加一个(防止设置超时时间失败导致问题) 
 
从上面这个逻辑来看问题不大,但是有个问题,case : 
如某个业务方setnx获取到了锁,但是因为网络问题,过了很久才获取到返回,此时锁已经失效并被其他业务方获取到了,就会出现多个业务方同时持有锁的场景 
 
III. 小结说明 想基于redis实现一个相对靠谱的分布式锁,需要考虑的东西还是比较多的,而且这种锁并不太适用于业务要求特别严格的地方,如
一个线程持有锁时,如果发生gc,导致锁超时失效,但是自己又不知道,此时就会出现多个业务方同时持有锁的场景 
对于锁超时的场景,需要仔细考虑,是否会出现并发问题 
确保只能释放自己的锁(以防止释放了别人的锁,出现问题) 
 
参考链接 
V. 其他 声明 尽信书则不如,已上内容,纯属一家之言,因本人能力一般,见解不全,如有问题,欢迎批评指正
扫描关注,java分享