image-20240311194503493

​ 以上这张图描述了这样一种场景:线程1在获取分布式锁后进入业务阻塞状态,并且阻塞时间甚至超过了我们设置的锁过期时间。在此期间,线程2成功获取了锁并开始执行业务,此时被阻塞的线程1又恢复运行并完成了业务,于是按照代码逻辑线程1会去释放锁,此时另一个线程3又来获取锁,造成了线程安全问题。

​ 出现该问题的本质原因在于在编写锁的释放代码时,并没有添加判断条件,这样做的漏洞就是释放的锁不一定是自己的锁。解决的方案也很简单,只需要在释放锁之前判断是否是自己的锁。可以使用UUID+线程id作为判断的标识,需要注意的是这种标识依旧无法做到完全唯一。如果想要实现标识完全唯一,可以使用每台电脑的MAC+线程id作为标识。本项目使用的是第一种方案

1
2
3
4
5
6
7
8
9
10
11
12
   private static final String LOCK_PREFIX = UUID.randomUUID().toString(true);

/**
* 释放锁
*/
public void unLock(){
String threadId = LOCK_PREFIX + "-" + Thread.currentThread().getId();
String flag = stringRedisTemplate.opsForValue().get(name);
if(flag.equals(threadId)) {
stringRedisTemplate.delete(name);
}
}

​ 在某些极端情况下,当前代码还会出现问题。

image-20240311203651449

​ 上图表示的情景是线程1判断锁标识后想要执行下一步时,突然发生的阻塞,导致锁无法正常释放,因此又发生了线程安全问题。出现该问题的原因在于,编写释放锁的代码时,判断和释放是两个动作,不能保证操作的原子性。

​ 实现Redis操作原子性的一种方案为使用Lua语言,这是一种脚本语言,并且可以使用该语言对Redis进行操作。以下网站是一个关于Lua语言的学习网站。

Lua学习网站

1
2
3
4
5
6
--利用lua脚本实现redis语句的原子性
if(redis.call("get",KEYS[1]) == ARGV[1])
then
return redis.call('del',KEYS[1])
end
return 0
1
2
3
4
5
6
7
8
9
10
11
12
13
  	//redis中提供的操作脚本语言的类
private static DefaultRedisScript redisScript;
static{
redisScript = new DefaultRedisScript();
redisScript.setLocation(new ClassPathResource("unlock.lua"));
redisScript.setResultType(Long.class);
}
/**
* 利用lua脚本释放锁
*/
public void unLock(){
stringRedisTemplate.execute(redisScript, Collections.singletonList(KEY_PREFIX + name), lockId);
}

​ 通过本节需要掌握:

  1. 在释放锁时需要考虑这个锁是否是自己的锁
  2. 学会使用lua语言保证执行redis语句的的原子性