问题场景:假设redis分布式锁(key)过期时间是5s,任务的执行时间是10s,那么就意味着A线程获取锁之后尚未执行完毕,B线程就可以获取到锁,很明显此时锁无法保证线程安全,应该如何优化处理?
思路:线程获取到锁之后,开启一个守护线程,专门用来维护key的过期时间。
代码如下:
@Autowired
RedisTemplate<String,String> redisTemplate;
//删除key的脚本
private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//释放锁
private Boolean unlock(String key,String value) {
return redisTemplate.execute((RedisCallback<Boolean>) connection -> {
Jedis jedis = (Jedis) connection.getNativeConnection();
Object eval = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(key), Collections.singletonList(value));
if (eval.equals(1L)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
//获取锁
private Boolean lock(String key, String value, long expireTime) {
Boolean lock = redisTemplate.execute((RedisCallback<Boolean>) connection -> {
Jedis jedis = (Jedis) connection.getNativeConnection();
String set = jedis.set(key, value, "NX", "EX", expireTime);
if ("OK".equals(set)) {
return true;
}
return false;
});
//如果获取锁成功,则开启守护线程
if(lock){
DaemonTask daemonTask = new DaemonTask(key,value,expireTime);
Thread thread = new Thread(daemonTask);
thread.setDaemon(true);
thread.start();
}
return lock;
}
//守护线程
pulic class DaemonTask implements Runnable{
private static final String POSTPONE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return '0' end";
private String key;
private String value;
private long expireTime;
private boolean isRunning;
public DaemonTask(String key, String value, long expireTime) {
this.key = key;
this.value = value;
this.expireTime = expireTime;
this.isRunning = Boolean.TRUE;
}
@Override
public void run() {
long waitTime = expireTime * 1000 * 2 / 3;// 线程等待多长时间后执行
while (isRunning){
try {
//守护线程等待 锁过期时间的2/3 后,
Thread.sleep(waitTime);
if(postpone(key,value,expireTime)){
System.out.println("延时成功...........................................................");
}else{
this.isRunning = false;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
//key过期时间维护
public Boolean postpone(String key, String value, long expireTime) {
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
Object result = jedis.eval(POSTPONE_LOCK_SCRIPT, Lists.newArrayList(key), Lists.newArrayList(value, String.valueOf(expireTime)));
if (result.equals(1L)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
}
//业务代码模拟
@RequestMapping("/lock")
public Result<?> expire(){
String key = "liuchs";
String value = UUID.randomUUID().toString();
long expireTime = 5L;
String threadName = Thread.currentThread().getName();
Boolean lock = lock(key,value,expireTime);
if(lock){
System.out.println(threadName+"---------业务线程获取锁-------------------------开始执行业务代码---"+ DateUtil.getDateStr(new Date(),"yyyy-MM-dd HH:mm:ss"));
try {
Thread.sleep(8000);
Boolean unlock = unlock(key, value);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(threadName+"-----------业务线程执行完毕------------------------------");
}else{
System.out.println(threadName+"---------------业务线程获取锁失败---------------");
return Result.renderError();
}
return Result.renderSuccess();
}
//运行结果分析
线程http-nio-8085-exec-1获取锁的时间为2023-06-28 17:26:05,任务的运行时间为8s,此时key的失效时间为 5s,正常情况下5s之后——2023-06-28 17:26:10之后,其他线程可以获取到锁,但是由于守护线程重新维护了key的过期时间,所以其他线程获取锁需要等待任务执行完毕——8s之后——2023-06-28 17:26:13之后,如下图:
由此可以保证任务线程未执行完毕的情况下,key不会过期。