乐趣区

关于redis:redis分布式锁自动延长过期时间

背景

项目组曾经有个 分布式锁 注解(参考前文《记一次分布式锁注解化》),然而在设置锁过期工夫时,须要去预估业务耗时工夫,如果锁的过期工夫能依据业务运行工夫主动调整,那应用的就更不便了。

思路

思路参考了redisson

  1. 保留原先的可自定义设置过期工夫,只有在 没有设置过期工夫(过期工夫为默认值 0)的状况下,才会启动主动缩短。
  2. 申请锁时,设置一个 缩短过期工夫 ,定时每隔 缩短过期工夫 的三分之一工夫就从新设置 过期工夫 期间工夫 值为 缩短过期工夫)。
  3. 为了避免某次业务因为异样而呈现 工作继续很久 ,从而长时间占有了锁,增加 最大延期次数 参数。

加锁

  1. 用一个 Map 来存储须要续期的 工作信息
  2. 在加锁胜利之后将 工作信息 放入 Map,并启动提早工作,提早工作在执行 延期动作 前先查看下 Map 里锁数据是不是还是被当前任务持有。
  3. 每次续期工作实现并且胜利之后,就再次启动提早工作。
申请锁

复用之前的 加锁 办法,把 缩短过期工夫 作为 加锁过期工夫

public Lock acquireAndRenew(String lockKey, String lockValue, int lockWatchdogTimeout) {return acquireAndRenew(lockKey, lockValue, lockWatchdogTimeout, 0);
}

public Lock acquireAndRenew(String lockKey, String lockValue, int lockWatchdogTimeout, int maxRenewTimes) {if (lockKey == null || lockValue == null || lockWatchdogTimeout <= 0) {return new Lock(this).setSuccess(false).setMessage("illegal argument!");
    }
    Lock lock = acquire(lockKey, lockValue, lockWatchdogTimeout);
    if (!lock.isSuccess()) {return lock;}
    expirationRenewalMap.put(lockKey, new RenewLockInfo(lock));
    scheduleExpirationRenewal(lockKey, lockValue, lockWatchdogTimeout, maxRenewTimes, new AtomicInteger());
    return lock;
}
定时续期

以后锁还未被开释(Map 里还有数据 ),并且以后 延期 工作执行胜利,则持续下一次工作。

private void scheduleExpirationRenewal(String lockKey, String lockValue, int lockWatchdogTimeout,
        int maxRenewTimes, AtomicInteger renewTimes) {ScheduledFuture<?> scheduledFuture = scheduledExecutorService.schedule(() -> {
        try {if (!renewExpiration(lockKey, lockValue, lockWatchdogTimeout)) {log.debug("dislock renew[{}:{}] fail!", lockKey, lockValue);
                return;
            }
            if (maxRenewTimes > 0 && renewTimes.incrementAndGet() == maxRenewTimes) {log.info("dislock renew[{}:{}] override times[{}]!", lockKey, lockValue, maxRenewTimes);
                return;
            }
            scheduleExpirationRenewal(lockKey, lockValue, lockWatchdogTimeout, maxRenewTimes, renewTimes);
        } catch (Exception e) {log.error("dislock renew[{}:{}] error!", lockKey, lockValue, e);
        }
    }, lockWatchdogTimeout / 3, TimeUnit.MILLISECONDS);
    RenewLockInfo lockInfo = expirationRenewalMap.get(lockKey);
    if (lockInfo == null) {return;}
    lockInfo.setRenewScheduledFuture(scheduledFuture);
}

private boolean renewExpiration(String lockKey, String lockValue, int lockWatchdogTimeout) {RenewLockInfo lockInfo = expirationRenewalMap.get(lockKey);
    if (lockInfo == null) {return false;}
    if (!lockInfo.getLock().getLockValue().equals(lockValue)) {return false;}
    List<String> keys = Lists.newArrayList(lockKey);
    List<String> args = Lists.newArrayList(lockValue, String.valueOf(lockWatchdogTimeout));
    return (long) jedisTemplate.evalsha(renewScriptSha, keys, args) > 0;
}
延期脚本
public void init() {
    ……
    String renewScript = "if redis.call('get',KEYS[1]) == ARGV[1] then \n" +
            "redis.call('pexpire', KEYS[1], ARGV[2]) \n" +
            "return 1 \n" +
            "end \n" +
            "return 0";
    renewScriptSha = jedisTemplate.scriptLoad(renewScript);
}

开释

执行 开释 之前,先将数据从 Map 里革除掉。

public boolean release(Lock lock) {if (!ifReleaseLock(lock)) {return false;}
    // 放在 redis 脚本后面,避免 redis 删除失败,而 map 没有清理,从而导致 redis 无限期续期
    try {RenewLockInfo lockInfo = expirationRenewalMap.get(lock.getLockKey());
        if (lockInfo != null) {ScheduledFuture<?> scheduledFuture = lockInfo.getRenewScheduledFuture();
            if (scheduledFuture != null) {scheduledFuture.cancel(false);
            }
        }
    } catch (Exception e) {log.error("dislock cancel renew scheduled[{}:{}] error!", lock.getLockKey(), lock.getLockValue(), e);
    }
    expirationRenewalMap.remove(lock.getLockKey());
    List<String> keys = Lists.newArrayList(lock.getLockKey());
    List<String> args = Lists.newArrayList(lock.getLockValue());
    return (long) jedisTemplate.evalsha(releaseScriptSha, keys, args) > 0;
}

注解革新

注解类

注解减少两个参数,并且原先的过期工夫参数默认值改为 0,即默认启动 主动延期

@Target(value = {ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface DisLock {

    int DEFAULT_EXPIRE = -1;
    int DEFAULT_LOCK_WATCHDOG_TIMEOUT = 30000;

    …… // 其余参数
    /**
     * 默认 key 过期工夫,单位毫秒
     *
     * @return long
     * @author
     * @date 2020-03-17 22:50
     */
    int expire() default DEFAULT_EXPIRE;

    /**
     * 监控锁的看门狗超时工夫,单位毫秒,参数用于主动续约过期工夫
     * 参数只实用于分布式锁的加锁申请中未明确应用 expire 参数的状况(expire 等于默认值 DEFAULT_EXPIRE)。*
     * @return int
     * @author
     * @date 2020-10-14 11:08
     */
    int lockWatchdogTimeout() default DEFAULT_LOCK_WATCHDOG_TIMEOUT;

    /**
     * 最大续期次数,用于避免业务过程迟缓在导致长时间占有锁
     *
     * @return int 大于 0 时无效,小于等于 0 示意无限度
     * @author
     * @date 2020-10-15 16:23
     */
    int maxRenewTimes() default 0;}
注解解决类
JedisDistributedLock.Lock lock = jedisDistributedLock.acquire(key, value, disLock.expire());

改成

JedisDistributedLock.Lock lock;
if (ifRenew(disLock)) {
    lock = jedisDistributedLock
            .acquireAndRenew(key, value, disLock.lockWatchdogTimeout(), disLock.maxRenewTimes());
} else {lock = jedisDistributedLock.acquire(key, value, disLock.expire());
}

protected boolean ifRenew(DisLock disLock) {return disLock.expire() == DisLock.DEFAULT_EXPIRE;
}
退出移动版