1 基于Redis分布式锁的问题
先来看看之前分布式锁的实现。
这个基于Redis的分布式锁仍然有着一个问题,那就是误删锁的问题。
简单的来说,就是当第一个线程,也就是线程1,拿到锁后,但由于本身业务复杂,而导致了阻塞,超过了锁设置的超时时间,锁自动释放。这个时候,线程2进来了,也拿到了锁,但是就在线程2执行业务的途中,线程1业务完成,主动释放了锁,又因为我们释放锁的逻辑是直接删除key,这就导致了线程2的锁被误删。
这就导致了线程安全的问题。
解决方法:在每个线程要释放锁的时候,主动判断reids中存入的线程标识,来判断是不是自己的锁,如果不是,就不能删除。
代码实现:
package com.hmdp.utils;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* @Author 华子
* @Date 2022/12/3 20:53
* @Version 1.0
*/
public class SimpleRedisLock implements ILock {
//Redis
private StringRedisTemplate stringRedisTemplate;
//业务名称,也就是锁的名称
private String name;
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
//key的前缀
private static final String KEY_PREFIX = "lock:";
//线程标识的前缀
private static final String ID_PREFIX = UUID.randomUUID().toString(true);
@Override
public boolean tryLock(long timeoutSec) {
//获取线程id,当作set的value
String threadId = Thread.currentThread().getId()+ID_PREFIX;
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
//释放锁
@Override
public void unlock() {
String threadId = Thread.currentThread().getId()+ID_PREFIX;
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
if (threadId.equals(id)){
//删除key
stringRedisTemplate.delete(KEY_PREFIX+name);
}
}
}
2 Lua脚本的使用
上述代码仍然会在极端的情况下仍然会出现误删锁的问题。
试想一下这种情况:
在线程判断完线程标识时,发现是自己的id,就在准备释放锁的时候,发生了阻塞。然后就是锁超时时间到了,别的线程有获取到了锁,又出现了误删的问题。
那么,又该如何解决这个问题呢?
我们不妨这样想想:
就是我们把判断id和删除id整合成一个代码,让他一次执行,不用分成两次,这样不就好了?
这里就要使用我们Redis中的脚本:Lua 。
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:Lua 教程 | 菜鸟教程
这里重点介绍Redis提供的调用函数,语法如下:
例如,我们要执行set name jack,则脚本是这样:
-- 执行reids命令
redis.call('set','name','jack')
使用redis来运行脚本
例如,我们要使用Redis调用脚本来执行set name jack,则脚本是这样:
EVAL "return redis.call('set', 'name', 'jack')" 0
注:后面的0是参数个数。
如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:
EVAL "return redis.call('set',KEYS[1],ARGV[1])" 1 name Rose
注:我们实际的业务就是需要这种,参数不能写死。
接下来,就是用Lua脚本编写代码逻辑
代码 :
-- 锁的key
-- local key = KEYS[1]
-- 线程标识
-- local threadId = ARGV[1];
--获取锁中的线程标识,get key
-- local id = redis.call('get',KEYS[1]);
--具体脚本
if(redis.call('get',KEYS[1]) == ARGV[1]) then
--释放锁 del key
return redis.call('del',KEYS[1])
end
return 0
然后在IDEA中编写此代码,并使用Redis调用。(注:编写Lua代码需下载EmmyLua插件)
具体实现代码:(主要看释放锁代码)
package com.hmdp.utils;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* @Author 华子
* @Date 2022/12/3 20:53
* @Version 1.0
*/
public class SimpleRedisLock implements ILock {
//Redis
private StringRedisTemplate stringRedisTemplate;
//业务名称,也就是锁的名称
private String name;
public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
this.stringRedisTemplate = stringRedisTemplate;
this.name = name;
}
//key的前缀
private static final String KEY_PREFIX = "lock:";
//线程标识的前缀
private static final String ID_PREFIX = UUID.randomUUID().toString(true);
//获取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);
}
@Override
public boolean tryLock(long timeoutSec) {
//获取线程id,当作set的value
String threadId = Thread.currentThread().getId()+ID_PREFIX;
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
//调用Lua脚本
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
Thread.currentThread().getId()+ID_PREFIX
);
}
}
主要还是使用了StringRedisTemplate中的excute方法,这里传一个脚本参数,一个key参数,一个value参数
1. 脚本参数:使用DefaultRedisScript 进行获取脚本,需要传入脚本文件的路径,调用new ClassPathResource("unlock.lua") 当做脚本文件路径
2. key参数,就是我们的KEYS[1],是一个集合,我们把我们使用的锁的key转成一个集合当成key参数
3. value,就是我们的线程标识。
这样,我们就使用的Lua脚本将原来两步需要实现的释放锁,合成一行代码实现,就不会出现问题啦~