首先我们要定义锁的实现;也就是下面的getLock方法;使用的是RedisTemplate;并没有去定义工具类。
getLock方法使用的是redis的SETNX命令;但是由于SETNX命令无法进行原子性操作过期时间;所以我们在这里使用SET命令;从redis2.6.12版本开始;SET命令的行为可以通过一系列参数来修改;
同时使用SET LOCK ID EX 30 NX命令的好处就是可以加上ID这个唯一标识。后续会讲到。
在获取锁的时候我们设置了key;也就是锁的名称;ID就是键值对的value。好处就是为了释放锁的时候出问题;举个例子;当线程A获取到锁进行业务逻辑的时候;线程B的误操作;直接释放了锁。为了解决类似的异常情况;我们将当前的线程ID和锁进行绑定。
设计这个方法的步骤和思想如下;
问题它又来了;这里的get;del命令又是两条命令了;又得回到原子性的问题
所以我们使用redis可以执行lua脚本的特性;写了一个脚本来原子性执行;解决这个问题。具体的实现如下代码所示;
;Component
public class RedisInfo {
;Autowired
private RedisTemplate<Object, Object> redisTemplate;
/**
* Title;getLock <br>
* Description;获取锁的实现 <br>
* author;于琦海 <br>
* date;2022/10/25 10:58 <br>
* ;param key 锁的名称
* ;param time 预估的过期时间
* ;param timeUnit 时间单位
* ;param thread 当前操作获取锁的线程
* ;return Boolean
*/
public Boolean getLock(String key, Long time, Thread thread, TimeUnit timeUnit) {
return redisTemplate.opsForValue().setIfAbsent(key, thread.getId(), time, timeUnit);
}
/**
* Title;release <br>
* Description;释放锁的原子实现 <br>
* author;于琦海 <br>
* date;2022/10/25 11:12 <br>
* ;param key 锁的名称
* ;param thread 当前操作释放锁的线程
* ;return Long
*/
public Long release(String key, Thread thread) {
String lua = ; if redis.call(;GET;,KEYS[1]) == ARGV[1] then return redis.call(;DEL;,KEYS[1]) else return 0 end;;
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setResultType(Long.class);
script.setScriptText(lua);
return redisTemplate.execute(script, Arrays.asList(key), thread.getId());
}
/**
* Title;ttl <br>
* Description;查询key的剩余过期时间 <br>
* author;于琦海 <br>
* date;2022/10/25 14:13 <br>
* ;param key 锁的名称
* ;param timeUnit 时间单位
* ;return Long
*/
public Long ttl(String key, TimeUnit timeUnit) {
return redisTemplate.getExpire(key, timeUnit);
}
/**
* Title;expire <br>
* Description;更新key的过期时间 <br>
* author;于琦海 <br>
* date;2022/10/25 14:33 <br>
* ;param key 锁的名称
* ;param time 预估的过期时间
* ;param timeUnit 时间单位
* ;return Boolean
*/
public Boolean expire(String key, long time, TimeUnit timeUnit) {
return redisTemplate.expire(key, time, timeUnit);
}
}
假设我们设置锁的持有时间为30s;但是由于网络问题或者机器问题导致执行我的业务逻辑超过了30s;而此时锁已经失效了;这种情况就会有问题。那如果我设置为一个超大的时间;比如100s一定能执行完;此时锁的效率又会变得非常低。
根据这个情况;我们可以写一个守护线程来根据剩余时间来对锁进行续期;类似于Redisson的看门狗机制。
思路如下;
;Service
public class RedisLockDemo {
;Autowired
private RedisInfo redisInfo;
/**
* Title;businessInfo <br>
* Description;这里面是业务逻辑伪代码 <br>
* author;于琦海 <br>
* date;2022/10/25 11:34 <br>
*/
public void businessInfo() {
// 获取锁
Boolean lock = redisInfo.getLock(;qhyu;, 30L, Thread.currentThread(), TimeUnit.SECONDS);
if (lock) {
try {
// 开启一个守护线程;去刷新过期时间,实现看门狗的机制
Runnable thread = () -> {
while (true) {
// 会不会
// 获取key的剩余过期时间
Long timeLess = redisInfo.ttl(;qhyu;, TimeUnit.SECONDS);
// 当时间小于等于10s的时候;续命
if (timeLess <= 10L) {
System.out.println(;续命;);
redisInfo.expire(;qhyu;, 30L, TimeUnit.SECONDS);
}
}
};
Thread thread1 = new Thread(thread);
thread1.setDaemon(true);
thread1.start();
int i = 0;
while (true) {
if (i < 50) {
i;;;
// 执行我们的业务逻辑
System.out.println(;这里是我的业务逻辑; ; i);
Thread.sleep(10000L);
} else {
break;
}
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
redisInfo.release(;qhyu;, Thread.currentThread());
}
}
}
}
根据上面的代码;写了个controller掉用了一下;能达到自动续期的目的。
;RestController
;RequestMapping((;/qhyu/redis;))
public class RedisLockController {
;Autowired
private RedisLockDemo redisLockDemo;
;GetMapping(;/test;)
public void test(){
redisLockDemo.businessInfo();
}
}
在时钟正常的情况下;我们预计没执行两次业务逻辑;就会续命一次。以下的结果能证明这一点。
那么如果不是守护线程;直接开一个线程不行吗?肯定是不行的;因为当线程执行完成之后;新开的线程不会被销毁;如下所示;会一直去对锁进行续命;就会出现问题。