分布式锁是满足分布式环境或者集群环境下多进程可见并且互斥的锁,分布式锁的核心思想就是让所有人都是用一把锁,这样就能够锁住线程,让线程能够串行化执行
分布式锁满足一些条件:可见性/高性能/互斥/高可用/安全性
我们使用Redis来实现分布式锁,本质上是利用Redis底下 SETNX 这条命令来实现分布式锁的。
这条命令表示: 当这个Key不存在时就创建这个Key并且返回TRUE,反之返回 FALSE。同时我们使用DEL这条命令来释放锁,我们可以设置超时时间来避免线程等待时长过久。
具体的工作流程是:当有多个线程进入时,我们就利用该方法,第一个线程进入时,redis 中就有这个key 了,返回了TRUE,如果结果是TRUE,则表示他抢到了锁,那么他去执行业务,然后再删除锁,退出锁逻辑,没有抢到锁的线程,等待一定时间后重试即可。
简单的代码实现如下
private static final String KEY_PREFIX="lock:"
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标示
String threadId = Thread.currentThread().getId()
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
public void unLock(String key){
stringRedisTemplate.delete(key);
}
@Override
public Result seckillVoucher(Long voucherId) {
//省略这部分业务代码......
//创建锁对象
SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
//获取锁对象
boolean isLock = lock.tryLock(1200);
//加锁失败
if (!isLock) {
return Result.fail("不允许重复下单");
}
try {
//省略这部分业务代码......
} finally {
//释放锁
lock.unlock();
}
}
我们刚刚写的代码实际上有点问题
假设有这种情况:持有锁的线程,这里称作 线程A 内部阻塞了导致锁自动释放了(超时自动释放),另一个线程B获取到了锁执行业务代码,突然线程A不再阻塞了,执行后面的代码就会执行到 lock.unlock() 这段代码,由于线程A已经不再持有锁了,所以线程A释放的是线程B的锁!这就是误删问题
那么应该如何解决呢?
方法也很简单,当线程释放锁的时候检查一下是不是自己的锁就可以了,如果是自己的锁就释放,如果不是自己的锁那就不用管了
我们用设置锁的时候给锁的名称加上线程ID,最后在释放锁的时候比对一下是不是自己的线程ID即可
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标示
String threadId = Thread.currentThread().getId()
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
public void unLock(String key){
// 获取线程标示
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现在持有锁之后,在执行业务逻辑过程中,他正准备删除锁,而且已经走到了条件判断的过程中,比如他已经拿到了当前这把锁确实是属于他自己的,正准备删除锁,也就是说走到了这一步
if(threadId.equals(id)){
//已经结束判断了但是还没有执行释放锁操作
stringRedisTemplate.delete(KEY_PREFIX + name);
}
但是此时他的锁到期了,那么此时线程2进来,但是线程1他会接着往后执行,当他卡顿结束后,他直接就会执行删除锁那行代码,相当于条件判断并没有起到作用,这就是删锁时的原子性问题!
为了保证所操作的原子性问题,我们可以使用Lua脚本来实现删除锁操作的原子性。
-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
-- 一致,则删除锁
return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0
使用JAVA代码来调用Lua脚本
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}
public void unlock() {
// 调用lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
实际上我们调用Redission就可以帮助我们实现以上的代码逻辑了,Redission是一种实现分布式锁(也实现了其他功能)的一种工具,Redission帮助我们封装好了Lua脚本等待,我们调用Redission可以轻松的帮助我们实现分布式锁
首先得先从Maven上拉取Redission相关的依赖
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.13.6version>
dependency>
然后在Config类底下配置好Redission相关的配置
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
//创建配置
Config config = new Config();
//设置连接地址
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
//创建RedissonClient对象
return Redisson.create(config);
}
}
最后直接使用即可
privaite Redission redissionClient;
@Override
public Result seckillVoucher(Long voucherId) {
//省略这部分业务代码......
//创建锁对象 设置锁的名称
RLock lock = redissionClient.getLock("anyLock);
//获取锁对象 参数的意思是: 获取锁的最大等待时间 锁自动释放时间 时间单位
boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
//加锁失败
if (!isLock) {
return Result.fail("不允许重复下单");
}
try {
//省略这部分业务代码......
} finally {
//释放锁
lock.unlock();
}
}