Redis distributed locks automatically extend expiration time

Time:2021-12-4

background

Project team already hasDistributed lockNote (refer to the previous note once distributed lock annotation), but when setting the lock expiration time, you need to estimate the business time-consuming time. If the lock expiration time can be automatically adjusted according to the business running time, it will be more convenient to use.

thinking

Thought referenceredisson

  1. Keep the original customizable expiration time, only inThe expiration time is not set (the default value is 0)The automatic extension will be started only when.
  2. When applying for a lock, set aExtend expiration time, timed everyExtend expiration timeReset in a third of the timeExpiration timePeriod timeValue isExtend expiration time)。
  3. In order to prevent a business from occurring due to an exceptionThe mission lasted a long time, thus occupying the lock for a long time and addingMaximum delay timesParameters.

Lock

  1. Use oneMapTo store information that needs to be renewedTask information
  2. After locking is successful, theTask informationPutMap, and start the delayed task. The delayed task is executingDelayed actionCheck beforeMapWhether the lock data is still held by the current task.
  3. Each time the renewal task is completed and successful, the delayed task is started again.
Apply for lock

Before reuseLockMethod, putExtend expiration timeAsLock expiration time

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;
}
Regular renewal

The current lock has not been released(There is data in the map), and currentlydelayIf the task is executed successfully, continue to the next task.

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;
}
Deferred script
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);
}

release

implementreleaseBefore, remove the data from theMapGet rid of it.

/**
 * @param lock
 * @return boolean
 */
public boolean removeRenew(Lock lock) {
    return expirationRenewalMap.remove(lock.getLockKey()) != null;
}

/**
 * @param lock
 */
public boolean release(Lock lock) {
    if (!ifReleaseLock(lock)) {
        return false;
    }
    //Put it in front of the redis script to prevent the deletion of redis from failing and the map is not cleaned up, resulting in the indefinite renewal of 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);
    }
    removeRenew(lock);
    List<String> keys = Lists.newArrayList(lock.getLockKey());
    List<String> args = Lists.newArrayList(lock.getLockValue());
    return (long) jedisTemplate.evalsha(releaseScriptSha, keys, args) > 0;
}

Annotation transformation

Annotation

Two parameters are added to the annotation, and the default value of the original expiration time parameter is changed to0, i.e. default startupautomatic prolongationAt the same time, pay attention toifExpireWhenFinishField compatibility.

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

    int DEFAULT_EXPIRE = -1;
    /**
     *The default renewal time plus the default renewal times, the result is: the default automatic renewal is one hour (30000 / 3 * 360)
     */
    int DEFAULT_LOCK_WATCHDOG_TIMEOUT = 30000;
    int DEFAULT_RENEW_TIMES = 360;

    ... // other parameters
    /**
     *Whether to delete the key after completion. If false,
     *1) non automatic renewal: the key will expire automatically without deleting it
     *2) automatic renewal: do not delete the key, but cancel the automatic renewal and let the key expire automatically
     *
     * @return boolean
     * @author
     * @date 2020-06-05 11:04
     */
    boolean ifExpireWhenFinish() default true;    
    /**
     *Default key expiration time, in milliseconds
     *
     * @return long
     * @author
     * @date 2020-03-17 22:50
     */
    int expire() default DEFAULT_EXPIRE;

    /**
     *Watchdog timeout of the monitoring lock, in milliseconds. The parameter is used to automatically renew the expiration time
     *The parameter is only applicable to the case where the expire parameter is not explicitly used in the lock request of the distributed lock (expire is equal to the default value default_exit).
     *
     * @return int
     * @author
     * @date 2020-10-14 11:08
     */
    int lockWatchdogTimeout() default DEFAULT_LOCK_WATCHDOG_TIMEOUT;

    /**
     *The maximum number of renewals is used to prevent the slow business process from occupying the lock for a long time
     *
     *@ return int is valid when it is greater than 0. If it is less than or equal to 0, there is no limit
     * @author
     * @date 2020-10-15 16:23
     */
    int maxRenewTimes() default DEFAULT_RENEW_TIMES;
}
Annotation processing class
JedisDistributedLock.Lock lock = jedisDistributedLock.acquire(key, value, disLock.expire());

Change to

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;
}

finalWhen the block releases the lock, it shall also be adjusted:

if (lock != null) {
    if (disLock.ifExpireWhenFinish()) {
        jedisDistributedLock.release(lock);
    } else if (ifRenew(disLock)) {
        jedisDistributedLock.removeRenew(lock);
    }
}