1. 说明
-
为避免设置的过期时间不足以完成业务逻辑,而选择开线程自动延长
key
的有效期 -
使用
Api
AtomicBoolean 原子性操作 // 比较相同则设置 boolean atomicBoolean.compareAndSet() // 如果 key 不存在 则设置 valueOperations.setIfAbsent() // 设置 key 过期时间 redisTemplate.expire()
2. 实现
-
WatchDogLock.java
import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import java.util.Objects; import java.util.concurrent.Executor; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; /** * 看门狗机制 锁 * * @author sheng_zs@126.com * @date 2021-08-09 11:19 */ @Slf4j public class WatchDogLock implements Lock { /** * redis 操作 */ private final RedisTemplate<String, Object> REDIS_TEMPLATE = SpringUtil.getBean("redisTemplate"); /** * redis String 操作 */ private final ValueOperations<String, Object> VALUE_OPERATIONS = SpringUtil.getBean("valueOperations"); /** * 线程池,SynchronousQueue:无缓冲等待队列,直接把任务交给消费者,必须等队列中的任务被消费才可以继续添加任务 * 为避免线程池采取拒绝策略,一般设置 maximumPoolSize 为 Integer.MAX_VALUE */ private final Executor EXECUTOR = new ThreadPoolExecutor(10, Integer.MAX_VALUE, 5L, TimeUnit.MINUTES, new SynchronousQueue<Runnable>()); /** * redis key */ private final String key; /** * 看门狗线程 */ private final WatchDogThread watchDogThread; /** * 是否已经加锁 */ private boolean isLock; public WatchDogLock(String key) { if (StrUtil.isBlank(key)) { throw new RuntimeException("key 不能为空"); } this.key = key; this.watchDogThread = new WatchDogThread(); } @Override public void lock() { // 无限期重试获取锁,并开启看门狗机制 lock(true, null, null, true); } @Override public void lockInterruptibly() throws InterruptedException { // 无限期重试获取锁 lock(true, null, null, false); } /** * 加锁 * * @param reTry 是否重试获取锁 * @param time 重试获取锁时间 * @param unit 重试获取时间的时间单位 * @param openWatch 是否开启看门狗机制 * @return 加锁成功返回 true */ private boolean lock(boolean reTry, Long time, TimeUnit unit, boolean openWatch) { // 开始时间,以及 重试获取锁的时间 long start = 0L, tryTime = 0L; // 是否有 重试时间 boolean hasTryTime; if (hasTryTime = Objects.nonNull(time) && Objects.nonNull(unit)) { start = System.currentTimeMillis(); tryTime = unit.toMillis(time); } // 设置到 redis 是否成功 Boolean flag; log.info("尝试获取锁"); do { flag = VALUE_OPERATIONS.setIfAbsent(key, true, 30L, TimeUnit.SECONDS); // 当 重试 && 设置不成功 && (没有重试时间 || 在重试时间内) } while (reTry && !Objects.equals(flag, true) && (!hasTryTime || System.currentTimeMillis() - start < tryTime)); // 获得锁后,开启看门狗线程,续命 boolean success; if (success = Objects.equals(flag, true)) { // 已经加锁 this.isLock = true; if (openWatch) { EXECUTOR.execute(watchDogThread); } } return success; } @Override public boolean tryLock() { // 不重试获取,如果获取锁成功,则开启看门狗机制 return lock(false, null, null, true); } @Override public boolean tryLock(long time, TimeUnit unit) { // time unit 时间内重试获取锁,如果获取成功,则开启看门狗机制 return lock(true, time, unit, true); } @Override public void unlock() { if (!isLock) { throw new RuntimeException("还未加锁"); } log.error("看门狗 set false"); watchDogThread.atomic.set(false); REDIS_TEMPLATE.delete(key); log.error("锁已释放"); } @Override public Condition newCondition() { throw new RuntimeException("不可创建 Condition"); } /** * 看门狗线程 */ class WatchDogThread implements Runnable { private final AtomicBoolean atomic = new AtomicBoolean(true); @Override public void run() { /* 休眠 10 秒,续命 10 秒 */ while (atomic.compareAndSet(true, true)) { try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { log.error("线程睡眠异常"); return; } log.error("看门狗线程-续命"); // 续命 10 秒 REDIS_TEMPLATE.expire(key, 30, TimeUnit.SECONDS); } } } }