一、思路
刚刚我们已经分析了redis分布式锁初级版本存在的问题以及对应的解决思路,其实关键点就是两个:
1、在获取锁时存入线程标示
2、在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致
从而避免误删别人的锁。
不过这里建议存入锁的时候使用的是UUID。
之前使用的是线程的id,线程id是一个递增的数字,JVM内部每创建一个线程,它的数字就会递增。但是如果我们是在集群的模式下,我们有多个JVM,这样一来,每个JVM内部都会维护这样一个递增的数字,那两个JVM很有可能出现线程id冲突的情况,所以说我们直接使用线程的id去作为线程标识是不够的,我们还要去区分不同的JVM,让它们产生一些差异,因此这里使用UUID。
事实上我们完全可以在去创建锁的时候整一个UUID,那这个锁的内部每来一个线程,我们再把线程ID拼接到后面,两者结合,这样用UUID来区分不同的JVM,再用线程ID来区分不同线程,两者结合,就能够去确保不同线程标识一定不一样,相同线程标识一定一样。
- 如果一致则释放锁
- 如果不一致则不释放锁
总结:在存入锁时,放入自己线程的标识,在删除锁时,判断当前这把锁的标识是不是自己存入的,如果是,则进行删除,如果不是,则不进行删除。
二、代码实现
获取锁
// 这里使用hutool工具类中的UUID,因为它里面有toString方法,里面传入true,可以将UUID里面的横线去掉
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
释放锁
public void unlock() {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁中的标示
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// 判断标示是否一致
if(threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
三、测试
VoucherOrderServiceImpl$seckillVoucher
在释放锁和获取锁的地方分别打个断点
然后Debug运行项目
将数据库中的库存改为100,然后清空所有订单
最后发起请求,同样也是法两个请求
回到IDEA中,跟之前一样,同样发了两个请求过来了。
现在我们要模拟的是其中一个超时了,然后我们去误删的情况,因此我们先让第一个去获取锁,可以发现它现在是获取锁成功的
回到redis中看一下,可以发现是有一把锁的,锁里面记录的就是UUID + 线程id的标识。
现在假设这把锁过期了,这里我们手动删一下,模拟它过期的这种情况
现在过期后,回到IDEA,现在再让 8082
放行,此时它来获取锁,因为之前的锁已经过期了,因此 8082
获取锁是成功的
接下来放行 8081
,此时它就会去释放锁的时候我们跟进去,进去后,8081
会获取自己的线程标识。
可以到redis中看一下,可以发现线程标识已经变了
接下来执行代码,它先得到自己的标识,然后得到锁中的标识,可以发现完全不一样
此时8081就知道了,这个锁不是我的,因此此时锁并没有被误删。
将8081放行,回到redis中,可以发现锁还在。
但是如果让8082执行释放锁的逻辑的时候,此时就可以真正释放锁了。
并且回到redis中访问,锁确实也不存在了。
所以我们通过添加线程标识,然后判断线程标识这样的一种机制,就已经解决了误删的问题,让我们的分布式锁变得更加的健壮了。
但是我们现在的分布式锁并不是一个完美的分布式锁,它还存在一些问题,具体什么问题下节继续分析。