Overselling is a hot and headache problem in both business and interview. This article records the author’s personal notes on learning from Redis.

The scene again

We all know that JVM-level synchronized cannot solve the oversold problem under distributed microservices. This level of locking only locks the current process and does not work with other microservices.

Use Redis to implement distributed locks

    // Identifies the lock on the current client
    String clienId = UUID.randomUUID().toString();
    String lockKey = "lockKey";
    // Lock and set the identifier and set the timeout
    Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clienId, 10, TimeUnit.SECONDS);
    if(! result) {return "error_code";
    }
    try {
        // Business code
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        // Check whether the lock exists on the current client
        if (clienId.equals(StringRedisTemplate.opsForValue().get(lockKey))) {
            // Release the lock if it exists
            stringRedisTemplate.delete("lockKey"); }}Copy the code

This is a simple implementation of distributed locks, but there are serious problems

If the business logic processing time > our own set timeout; Redis was released as a timeout, and another thread swooped in and grabbed the lock. But my current thread is ina finally statement to release the lock

If (clienId equals (StringRedisTemplate opsForValue () get (lockKey))) {/ / there is releasing the lock StringRedisTemplate. Delete (" lockKey "); }Copy the code

That’s where problems arise

Our requirements are

  • Our business logic may take longer to execute than the timeout, but we don’t want a timeout, we want an operation like the Internet cafe overtime
  • We want the lock release to be atomic

Which brings us to Redisson

Redisson

Github Redisson has come up with a guard dog mechanism to renew locks

Source code based on the current latest version of Redisson V3.16.3

We directly positioning of the core code: scheduleExpirationRenewal method

protected void scheduleExpirationRenewal(long threadId) {
   ExpirationEntry entry = new ExpirationEntry();
   // Return null if there is no value, or return the corresponding value
   ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
   if(oldEntry ! =null) {
       // It is ready
       oldEntry.addThreadId(threadId);
   } else {
       // For the first time
       entry.addThreadId(threadId);
       try {
           / / lock sustain her life
           renewExpiration();
       } finally {
           if (Thread.currentThread().isInterrupted()) {
               / / releases the lockcancelExpirationRenewal(threadId); }}}}Copy the code

The ExpirationEntry object is an operation object for which Redisson would renew a lock, similar to the beanDefinition object in Spring

ExpirationEntry

    public static class ExpirationEntry {

        // Store the threadId collection that needs to be renewed
        private final Map<Long, Integer> threadIds = new LinkedHashMap<>();
        // Set the timeout period
        private volatile Timeout timeout;

        / / to omit
    }
Copy the code

It is very clear from the source code that this data type is used to renew the objects

Then we look at the source code for the main renewExpiration core code

renewExpiration

    private void renewExpiration(a) {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
    
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }
            
            // Continue operation
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if(e ! =null) {
                    log.error("Can't update lock " + getRawName() + " expiration", e);
                    EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                    return;
                }
                
                if (res) {
                    // Reschedule itself recalls itself
                    renewExpiration();
                } else {
                    cancelExpirationRenewal(null); }}); } }, internalLockLeaseTime /3, TimeUnit.MILLISECONDS);
    
    ee.setTimeout(task);
}
Copy the code

It creates a child thread to call again and again; EntryName = EntryName = EntryName = EntryName = EntryName = EntryName = EntryName = EntryName = EntryName Otherwise, execute internalLockLeaseTime / 3 time renew operations at intervals. Execute renewationAsync at intervals

renewExpirationAsync

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return 1; " +
                    "end; " +
                    "return 0;",
            Collections.singletonList(getRawName()),
            internalLockLeaseTime, getLockName(threadId));
}
Copy the code

Redisson implements statement lock continuation through lua scripts, which extend the timeout of this value a bit longer, and because it is lua scripts, it has atomicity

cancelExpirationRenewal

CancelExpirationRenewal is called when we do not need to renew our lives

protected void cancelExpirationRenewal(Long threadId) {
    ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (task == null) {
        return;
    }
    
    if(threadId ! =null) {
        task.removeThreadId(threadId);
    }

    if (threadId == null || task.hasNoThreads()) {
        Timeout timeout = task.getTimeout();
        if(timeout ! =null) {
            timeout.cancel();
        }
        // Remove Entry from map that needs to be renewedEXPIRATION_RENEWAL_MAP.remove(getEntryName()); }}Copy the code

Know that as soon as I saw the last sentence is simply to remove him, we started to scheduleExpirationRenewal before finally also see this in the block

How the watchdog got its name

Let’s look for the assignment of this interval internalLockLeasetime

private long lockWatchdogTimeout = 30 * 1000;
Copy the code

You can see that he defaults to 30 seconds, which is 10 seconds apart, to determine whether he needs to renew his life