Redission分布式锁
一、Redis分布式锁存在的问题
在我的这篇文章中,提到了使用Redis的setnx的原理,来实现分布式锁,但其实这种方式依旧存在着一些隐患
1.1 误删锁
逻辑说明:
当线程1持有锁并且在进行业务时,业务出现了长时间的阻塞以致于超过了锁的有效时间,导致锁自动释放了。
然后其他线程(假设为线程2)就趁虚而入,获取到了锁并执行业务,执行完业务后,就吧锁给释放了。
这就出现了其他线程释放了原来线程的锁的情况
分析:
出现这种问题的原因是,获取锁和释放锁的时候,我们都是根据锁的key来找到锁,然后释放的。
而我们命名key的时候,只是根据“lock:”+业务名称来命名的,而锁的value,我们是用1来表示锁被占用的。
所以在释放锁的时候,只要是其他线程执行同样的业务时根据一样的key获取到了当前的被超时释放的处于空闲状态的锁,那么他就能释放当前的锁。
所以我们要把value的值,改为线程的ID,然后在释放锁的时候,判断一下根据key获取到的value是否跟当前线程的ID一样
一样:释放锁
不一样:不能释放锁
那么就有人要问了:为什么不是把key 改为“lock:“+业务名称+线程id 呢?
你这样改了,你这个锁还有什么意义,一个锁跟一个线程一一对应,一个锁永远只有一个线程来获取,然后其他线程来执行这个业务的时候又会获得一个新的锁,这样子这个把锁有没有都没什么区别了。因为同一时间内都可以有很多线程来执行这个业务。
这样就完了吗? 在单机环境下,每个线程的ID确实都会不一样,这样确实可以避免误删锁的情况。但是,如果是分布式的环境下呢?
机子1的JVM产生的线ID绝对不会相同,机子2的JVM产生的线程ID绝对不会相同,但是机子1产生的线程ID可能会和机子2的线程ID相同,从而又会导致误删锁的情况发生
这时候我们还可以在用一个UUID来拼接value,UUID的生成跟机器码还有时间有关,所以不同机子上产生的UUID绝对不一样。
解决问题:
引入工具依赖以便于产生UUID
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
加锁:
private static final String KEY_PREFIX="lock:"
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";//用toString是因为redis里的value存的就是String类型的,所以要转换
@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);
}
}
1.2 原子性问题
问题说明:
在某种极端的情况下,假设线程1获取了锁,并执行完了业务,在判断value标示和自身线程标示一致后,准备要释放锁前,此时锁突然过期了,此时其他线程(假设为线程2)就会趁虚而入,获取到锁,随后执行线程2的业务,然后线程1又会继续释放锁的代码,这样也会出现误删锁的情况
问题分析:
出现这种问题的主要原因,就是在释放锁的时候,我们没有做到同时判断同时执行释放操作,没有保持原子性。
我们上面的代码中,是先判断value和当前线程的标示是否一致,再释放锁的。有一个先后的顺序,那么就有可能出现其他线程趁虚而入的问题。
那么可能就会有人说,要不然试试@Transactional,不就可以保证一致性了吗。首先,@Transactional并不能保证多条操作同时成功,还是有先后顺序的,事务只是保证了一系列操作要么都成功要么都失败,但是在成功的时候并不会保证都是同时成功,其次,一般@Transactional只用于关系型数据库,redis对事务的支持也不是特别完善。而且这里redis中出现的问题并不是因为redis出现了错误,只是操作之间有间隙,导致其他线程趁虚而入了。
所以我们一般使用lua脚本保证redis的多条命令的一致性
解决问题:
一般我们使用lua脚本来保证redis多条命令的原子性。即把多条命令当成一个整体执行,只要一条执行不成功那么所以命令都不会成功。所有命令都能执行成功的时候,那么所有命令就能同时成功。