1、目标
本文的主要目标是探究Redisson分布式锁在设置过期时间的情况下多线程是否会误删除的问题,首先分析单线程执行的完整过程,然后分析多线程锁误删除的现象,接着进行源码分析,理解Redisson如何保证多线程场景下当前线程不会误删除其他线程id的锁,最后是总结
2、单线程执行的完整过程
为了研究多线程场景下redisson分布式锁的执行流程,可以先做一个单线程的demo
引入pom.xml文件的依赖
<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置redisson
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://ip:port");
return Redisson.create(config);
}
}
测试redisson的controller
@RestController
@RequestMapping("/testRedisson")
@RequiredArgsConstructor
@Log4j2
public class TestRedissonController {
private final StringRedisTemplate stringRedisTemplate;
private final RedissonClient redissonClient;
@GetMapping("/f1")
public String f1() throws Exception {
RLock lock = redissonClient.getLock("redisson.lock");
tryLockBefore(lock);
boolean isLock = lock.tryLock(8, 10, TimeUnit.SECONDS);
if (!isLock) {
return "f2 tryLock failed";
}
tryLockAfter(lock);
try {
// 业务
Thread.sleep(1000 * 8);
} finally {
releaseLockBefore(lock);
lock.unlock();
releaseLockAfter(lock);
}
return "f2 ok";
}
private void tryLockBefore(RLock lock) throws Exception {
List<Object> list = isExistRedissonKey(lock);
log.info("threadId: {}, tryLock before, hash key isExist = {}, hash field isExist = {}, hash all fields and values = {}",
Thread.currentThread().getId(), list.get(0), list.get(1), list.get(2));
}
private void tryLockAfter(RLock lock) throws Exception {
List<Object> list = isExistRedissonKey(lock);
log.info("threadId: {}, tryLock after, hash key isExist = {}, hash field isExist = {}, hash all fields and values = {}",
Thread.currentThread().getId(), list.get(0), list.get(1), list.get(2));
}
private void releaseLockBefore(RLock lock) throws Exception {
List<Object> list = isExistRedissonKey(lock);
log.info("lock.isHeldByCurrentThread() : {}", lock.isHeldByCurrentThread());
log.info("threadId: {}, releaseLock before, hash key isExist = {}, hash field isExist = {}, hash all fields and values = {}",
Thread.currentThread().getId(), list.get(0), list.get(1), list.get(2));
}
private void releaseLockAfter(RLock lock) throws Exception {
List<Object> list = isExistRedissonKey(lock);
log.info("threadId: {}, releaseLock after, hash key isExist = {}, hash field isExist = {}, hash all fields and values = {}",
Thread.currentThread().getId(), list.get(0), list.get(1), list.get(2));
}
private List<Object> isExistRedissonKey(RLock lock) throws Exception {
RedissonObject redissonObject = (RedissonObject) lock;
Class<RedissonObject> cls = RedissonObject.class;
Field field = cls.getDeclaredField("name");
field.setAccessible(true);
String name = (String) field.get(redissonObject);
//log.info("name = {}", name);
RedissonLock redissonLock = (RedissonLock) lock;
Class<? extends RedissonLock> aClass = redissonLock.getClass();
Method method = aClass.getDeclaredMethod("getLockName", long.class);
method.setAccessible(true);
String lockName = (String) method.invoke(redissonLock, Thread.currentThread().getId());
//log.info("lockName = {}", lockName);
Boolean b1 = stringRedisTemplate.hasKey(name);
Boolean b2 = stringRedisTemplate.opsForHash().hasKey(name, lockName);
Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(name);
//log.info("b1 = {}, b2 = {}", b1, b2);
List<Object> res = new ArrayList<>(2);
res.add(b1);
res.add(b2);
res.add(JSON.toJSONString(map));
return res;
}
}
这个controller是在tryLock方法前后、unlock方法前后分别打印redisson底层存储的获取锁信息,它是一个hash类型数据,打印数据的思路是利用反射调用redisson底层存储数据的非公有属性和方法,具体实现在“4、源码分析”中讲解
单线程执行会打印如下信息
(1)执行tryLock方法会获取锁,将获取锁的信息存储到一个hash类型的数据中,这个hash类型数据的key是执行getLock(“redisson.lock”)方法中锁的名称redisson.lock,这个hash类型数据的field是lockName,它包含获取锁时当前线程id,这个hash类型数据的value是当前线程id获取锁的锁重入次数
① 打印信息中tryLock before会展示这个hash类型的key对应的所有fields和所有values数据,tryLock before数据为空表示执行tryLock方法之前hash类型中没有field和value
② 打印信息中tryLock after会展示这个hash类型的key对应的所有fields和所有values数据,field是"e4f10c81-bec7-4997-b410-569017db6e2d:75",其中75是当前线程id,value是1,即线程id为75的线程获取锁的锁重入次数,tryLock after表示在执行tryLock方法之后已经将75和1存储到hash类型中
(2)代码中这个线程执行业务的时间是8秒,redisson锁的超时时间是10秒,可以正常执行unlock方法
① 打印信息中releaseLock before会展示这个hash类型的key对应的所有fields和所有values数据,releaseLock before可以看到存储的数据仍然是tryLock after保存的数据
② 打印信息中releaseLock after会展示这个hash类型的key对应的所有fields和所有values数据,releaseLock after数据为空表示执行unlock方法之后已经删除了hash类型中线程id为75的数据
3、多线程锁误删除的现象
在单线程场景下执行tryLock方法redisson可以将数据保存到一个hash类型的数据中,执行unlock方法redisson会删除hash类型中对应线程id的field和value数据,但是多线程场景下如果设置了过期时间并且业务执行时间过长可能会导致多线程锁的误删除,锁的误删除可能会导致一些问题比如共享数据的并发修改,多线程锁的误删除可能出现的场景如下图
线程1获取锁设置过期时间是5秒,业务执行时间是8秒,线程2获取锁设置过期时间是10秒,业务执行时间是5秒,可能出现的问题是线程1获取锁成功,线程2等待锁被释放,线程1在5秒的时候锁过期了就会自动释放锁,线程2就获取锁成功,但是线程1的业务还没有执行完成就会一直执行直到业务执行结束后执行unlock方法,此时释放的锁是获取锁成功的线程2的锁,会造成线程2不持有锁了,从而导致共享数据的并发修改问题
4、源码分析
4.1 执行tryLock方法
执行tryLock方法会判断hash类型数据是否存在,如果不存在就会获取锁成功并添加数据到hash,如果存在就判断hash有没有当前线程id,如果有就获取锁成功并将当前线程id的锁重入次数加1,如果hash没有当前线程id就会返回这个hash的剩余过期时间表示获取锁失败
其中,这里设置了当前线程id的过期时间,只有设置过期时间然后当过期时间到了之后才会删除hash的field和value,如果没有设置过期时间过期时间默认是-1如果业务没有执行完成就会自动续期,这是tryLock的另一个逻辑,field是当前线程id的lockName,value是当前线程id的锁重入次数
hash类型数据的key是RedissonObject的name属性,因此需要采用反射获取属性
hash类型数据的field是RedissonLock的getLockName方法,因此需要采用反射获取方法
4.2 执行unlock方法
执行unlock方法会判断hash类型数据中field是当前线程id的是否存在,如果不存在就返回空,这样就可以避免锁的误删除,即避免当前线程id删除其他线程id的锁信息
如果存在就将锁重入次数减1,然后判断锁重入次数是否为0,如果大于0就重新设置过期时间,如果等于0就释放锁并发布锁被释放的消息
当前线程id想要删除其他线程id的锁信息会抛出异常
4.3 多线程测试controller
@RestController
@RequestMapping("/testRedisson")
@RequiredArgsConstructor
@Log4j2
public class TestRedissonController {
private final StringRedisTemplate stringRedisTemplate;
private final RedissonClient redissonClient;
@GetMapping("/f1")
public String f1() throws Exception {
RLock lock = redissonClient.getLock("redisson.lock");
tryLockBefore(lock);
boolean isLock = lock.tryLock(10, 5, TimeUnit.SECONDS);
if (!isLock) {
return "f2 tryLock failed";
}
tryLockAfter(lock);
try {
// 业务
Thread.sleep(1000 * 8);
} finally {
releaseLockBefore(lock);
lock.unlock();
releaseLockAfter(lock);
}
return "f1 ok";
}
@GetMapping("/f2")
public String f2() throws Exception {
RLock lock = redissonClient.getLock("redisson.lock");
tryLockBefore(lock);
boolean isLock = lock.tryLock(10, 10, TimeUnit.SECONDS);
if (!isLock) {
return "f2 tryLock failed";
}
tryLockAfter(lock);
try {
// 业务
Thread.sleep(1000 * 5);
} finally {
releaseLockBefore(lock);
lock.unlock();
releaseLockAfter(lock);
}
return "f2 ok";
}
private void tryLockBefore(RLock lock) throws Exception {
List<Object> list = isExistRedissonKey(lock);
log.info("threadId: {}, tryLock before, hash key isExist = {}, hash field isExist = {}, hash all fields and values = {}",
Thread.currentThread().getId(), list.get(0), list.get(1), list.get(2));
}
private void tryLockAfter(RLock lock) throws Exception {
List<Object> list = isExistRedissonKey(lock);
log.info("threadId: {}, tryLock after, hash key isExist = {}, hash field isExist = {}, hash all fields and values = {}",
Thread.currentThread().getId(), list.get(0), list.get(1), list.get(2));
}
private void releaseLockBefore(RLock lock) throws Exception {
List<Object> list = isExistRedissonKey(lock);
log.info("lock.isHeldByCurrentThread() : {}", lock.isHeldByCurrentThread());
log.info("threadId: {}, releaseLock before, hash key isExist = {}, hash field isExist = {}, hash all fields and values = {}",
Thread.currentThread().getId(), list.get(0), list.get(1), list.get(2));
}
private void releaseLockAfter(RLock lock) throws Exception {
List<Object> list = isExistRedissonKey(lock);
log.info("threadId: {}, releaseLock after, hash key isExist = {}, hash field isExist = {}, hash all fields and values = {}",
Thread.currentThread().getId(), list.get(0), list.get(1), list.get(2));
}
private List<Object> isExistRedissonKey(RLock lock) throws Exception {
RedissonObject redissonObject = (RedissonObject) lock;
Class<RedissonObject> cls = RedissonObject.class;
Field field = cls.getDeclaredField("name");
field.setAccessible(true);
String name = (String) field.get(redissonObject);
//log.info("name = {}", name);
RedissonLock redissonLock = (RedissonLock) lock;
Class<? extends RedissonLock> aClass = redissonLock.getClass();
Method method = aClass.getDeclaredMethod("getLockName", long.class);
method.setAccessible(true);
String lockName = (String) method.invoke(redissonLock, Thread.currentThread().getId());
//log.info("lockName = {}", lockName);
Boolean b1 = stringRedisTemplate.hasKey(name);
Boolean b2 = stringRedisTemplate.opsForHash().hasKey(name, lockName);
Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(name);
//log.info("b1 = {}, b2 = {}", b1, b2);
List<Object> res = new ArrayList<>(2);
res.add(b1);
res.add(b2);
res.add(JSON.toJSONString(map));
return res;
}
}
测试controller设置f1方法和f2方法,f1方法获取锁设置过期时间是5秒,业务执行时间是8秒,f2方法获取锁设置过期时间是10秒,业务执行时间是5秒,先调用f1方法,然后调用f2方法,会出现f1方法的线程执行unlock方法想要删除f2方法线程id的锁,导致抛出一个锁不能误删除的异常,从而避免了f1方法线程删除f2方法线程id的锁
测试controller在tryLock方法的前后和unlock方法的前后分别查询hash类型的数据
查询hash类型的一个key对应的所有fields和values数据用stringRedisTemplate.opsForHash().entries(name)方法,其中key是name锁的名字,field是线程id的lockName,value是这个线程id对应锁重入次数
key通过反射RedissonObject的name属性,field通过反射RedissonLock的getLockName方法
4.4 多线程测试controller测试结果
线程74获取锁成功,等待线程74的锁超时自动释放锁,线程76获取锁成功,当线程74的业务执行完成后会执行unlock方法,不会误删除线程76的锁,而是抛出异常
当线程76的业务执行完成后会执行unlock方法,线程76删除自己的锁成功,这表示线程76的锁没有被线程74误删除
5、总结
多线程场景下Redisson锁不会被误删除指的是当前线程id不会删除其他线程id的锁,这是通过Redisson存储的一个hash类型数据,记录了获取锁成功的线程id和锁重入次数,释放锁的时候会判断当前线程id是否在hash类型的field中,如果不在就不能删除,这样就保证了锁不会被其他线程误删除
标签:误删除,Redisson,hash,get,lock,list,线程,id From: https://blog.csdn.net/weixin_43823462/article/details/140228516