查看了很多关于redis实现分布式锁的文章,无疑要设计一个靠谱的分布式并不太容易,总会出现各类鬼畜的问题;如今就来小述一下,在设计一个分布式锁的过程当中,会遇到一些什么问题java
借助redis来实现分布式锁(咱们先考虑单机redis的模式),首先有必要了解下如下几点:redis
获取锁:安全
释放锁:服务器
网上一种常见的case,主要思路以下网络
实现代码以下并发
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相同app
上面的实现方式,一个混乱的case以下:dom
实际验证分布式
在上面的代码中,配合测试case,加上一些日志输出测试
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!"); // 人工接入,确保t1 获取到锁, t2 获取的是t1设置的内容, t3获取的是t2设置的内容 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)) { // t1获取到锁,强制sleep40ms, 确保线t2,t3也进入了 getSet逻辑 Thread.sleep(40); } return value; } else { // 表示返回的是其余业务设置的锁,赶忙的设置回去 // 人肉介入,确保t2优先执行,并设置回t1设置的值, t3后执行设置的是t2设置的值 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
@Test public void testLock() throws InterruptedException { // 先无视获取jedis的方式 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); };
部分输出结果:
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给覆盖
如何解决上面这个问题呢?
上面是典型的并发致使的问题,固然能够考虑从解决并发问题的角度出发来考虑,一个常见的方式就是加锁了,思路以下:(不详细展开了)
这种实现方式,会有如下的问题:
相比于前面一种直接将value设置为时间戳,而后来比对的方法,这里则直接借助redis自己的expire方式来实现超时设置,主要实现逻辑相差无几
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); // 主动设置超时时间为3s 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 :
想基于redis实现一个相对靠谱的分布式锁,须要考虑的东西仍是比较多的,并且这种锁并不太适用于业务要求特别严格的地方,如
尽信书则不如,已上内容,纯属一家之言,因本人能力通常,看法不全,若有问题,欢迎批评指正