redisson implements the principle of distributed locking

  • 2020-06-03 06:39:16
  • OfStack

Redisson distributed locks

One of the previous annotation-based locks is the basic redis distributed lock. The implementation of the lock is based on RLock provided by the redisson component.

Different versions of the lock implement different mechanisms

According to the recently released version 3.2.3 of redisson, different versions may implement different locking mechanisms. Earlier versions seem to be configured with simple setnx, getset and other general commands, while later redis has changed the implementation principles due to its support for script Lua.


<dependency>
 <groupId>org.redisson</groupId>
 <artifactId>redisson</artifactId>
 <version>3.2.3</version>
</dependency>

setnx needs to cooperate with getset and transactions to complete, so as to better avoid the deadlock problem. However, the new version can avoid using transactions and operating multiple redis commands because it supports lua scripts, and the semantic expression is clearer.

Features of the RLock interface

Inherits the standard interface Lock

Has all the features of the standard lock interface, such as lock,unlock,trylock, and so on.

Extends the standard interface Lock

Extended a number of methods, commonly used are: forced lock release, locks with expiration dates, and a set of asynchronous methods. The first two of these approaches focus on resolving possible deadlocks caused by the standard lock. For example, after a thread acquires the lock, the machine in which the thread is located crashes. At this time, the thread that acquired the lock cannot release the lock normally, causing the remaining thread 1 waiting for the lock to wait.

Reentrant mechanism

The implementation of each version is different, reentrant mainly considers performance, the same as 1 thread does not need to go through the application process if it applies for the lock resource again without releasing the lock, it only needs to continue to return the acquired lock and record the number of reentrant, which is similar to the ReentrantLock function in jdk. The reentrant count is matched by the hincrby command, detailed in the code below the parameters.

How can I tell if it's the same thread as 1?

The scheme of redisson is that one guid instance of RedissonLock plus id of the current thread is returned via getLockName


public class RedissonLock extends RedissonExpirable implements RLock {
 final UUID id;
 protected RedissonLock(CommandExecutor commandExecutor, String name, UUID id) {
  super(commandExecutor, name);
  this.internalLockLeaseTime = TimeUnit.SECONDS.toMillis(30L);
  this.commandExecutor = commandExecutor;
  this.id = id;
 }
 String getLockName(long threadId) {
  return this.id + ":" + threadId;
 }

RLock obtains locks in two scenarios

Here take the source code of tryLock: tryAcquire method is to apply for a lock and return the remaining time of lock validity. If the lock is empty, it means that the lock has not been obtained directly by other threads. If the time is obtained, it enters the waiting competition logic.


public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
  long time = unit.toMillis(waitTime);
  long current = System.currentTimeMillis();
  final long threadId = Thread.currentThread().getId();
  Long ttl = this.tryAcquire(leaseTime, unit);
  if(ttl == null) {
   // Get the lock directly 
   return true;
  } else {
   // There is competition to follow 
  }
 }

No competition, direct access to the lock

Taking a look at what redis is doing behind the lock that was first acquired and released, you can use monitor of redis to monitor redis execution in the background. When we add @RequestLockable to the method, we call lock and unlock. Here is the redis command:

lock

Because the high version of redis supports lua script, SO redisson also supports it and adopts the script mode. Those who are not familiar with lua script can find it. The logic for executing the lua command is as follows:


<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    this.internalLockLeaseTime = unit.toMillis(leaseTime);
    return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "if (redis.call(\'exists\', KEYS[1]) == 0) then redis.call(\'hset\', KEYS[1], ARGV[2], 1); redis.call(\'pexpire\', KEYS[1], ARGV[1]); return nil; end; if (redis.call(\'hexists\', KEYS[1], ARGV[2]) == 1) then redis.call(\'hincrby\', KEYS[1], ARGV[2], 1); redis.call(\'pexpire\', KEYS[1], ARGV[1]); return nil; end; return redis.call(\'pttl\', KEYS[1]);", Collections.singletonList(this.getName()), new Object[]{Long.valueOf(this.internalLockLeaseTime), this.getLockName(threadId)});
  }

Process of locking:

Determine whether the lock key exists, there is no direct call to hset to store the current thread information and set the expiration time, return nil, tell the client to get the lock directly. To determine whether the lock key exists, increment the number of reentrant times by 1, reset the expiration time, return nil, and tell the client to get the lock directly. Has been locked by another thread, returns the remaining time of lock validity, and tells the client to wait.

"EVAL" 
"if (redis.call('exists', KEYS[1]) == 0) then 
redis.call('hset', KEYS[1], ARGV[2], 1); 
redis.call('pexpire', KEYS[1], ARGV[1]); 
return nil; end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
redis.call('hincrby', KEYS[1], ARGV[2], 1); 
redis.call('pexpire', KEYS[1], ARGV[1]); 
return nil; end;
return redis.call('pttl', KEYS[1]);"
 "1" "lock.com.csp.product.api.service.ProductAppService.searchProductByPage#0" 
 "1000" "346e1eb8-5bfd-4d49-9870-042df402f248:21"

The lua script above is converted to the actual redis command, and the redis command below is actually executed after the lua script operation.


1486642677.053488 [0 lua] "exists" "lock.com.csp.product.api.service.ProductAppService.searchProductByPage#0"
1486642677.053515 [0 lua] "hset" "lock.com.csp.product.api.service.ProductAppService.searchProductByPage#0" 
"346e1eb8-5bfd-4d49-9870-042df402f248:21" "1"
1486642677.053540 [0 lua] "pexpire" "lock.com.csp.product.api.service.ProductAppService.searchProductByPage#0" "1000"

unlock

The process of unlocking looks a little more complicated:

If the lock key does not exist, the message says the lock is available nil is returned if the lock is not locked by the current thread Since reentrancy is supported, the number of reentrancy times needs to be reduced by 1 when unlocking If you calculate the number of reentries > 0, then reset the expiration time If you calculate the number of reentries < =0, the message says the lock is available

"EVAL" 
"if (redis.call('exists', KEYS[1]) == 0) then
 redis.call('publish', KEYS[2], ARGV[1]);
 return 1; end;
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then 
return nil;end; 
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); 
if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; 
else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; 
return nil;"
"2" "lock.com.csp.product.api.service.ProductAppService.searchProductByPage#0" 
"redisson_lock__channel:{lock.com.csp.product.api.service.ProductAppService.searchProductByPage#0}"
 "0" "1000"
 "346e1eb8-5bfd-4d49-9870-042df402f248:21"

Unlocking redis command without competition:

This is done by sending an unlocked message to wake up the thread in the waiting queue to re-compete for the lock.


1486642678.493691 [0 lua] "exists" "lock.com.csp.product.api.service.ProductAppService.searchProductByPage#0"
1486642678.493712 [0 lua] "publish" "redisson_lock__channel:{lock.com.csp.product.api.service.ProductAppService.searchProductByPage#0}" "0"

There's competition. Wait

The competing lua scripts on the redis end are the same, but different conditions execute different redis commands, complicated on the redisson source code. When it is discovered through tryAcquire that a lock has been requested by another thread, it needs to enter the wait contention logic.

this. await returns false, indicating that the wait time has exceeded the maximum wait time for acquiring the lock. Unsubscribe and return the acquisition lock failed this. await returns true, enters the loop and attempts to acquire the lock.

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    long time = unit.toMillis(waitTime);
    long current = System.currentTimeMillis();
    final long threadId = Thread.currentThread().getId();
    Long ttl = this.tryAcquire(leaseTime, unit);
    if(ttl == null) {
      return true;
    } else {
      // But the point is this 
      time -= System.currentTimeMillis() - current;
      if(time <= 0L) {
        return false;
      } else {
        current = System.currentTimeMillis();
        final RFuture subscribeFuture = this.subscribe(threadId);
        if(!this.await(subscribeFuture, time, TimeUnit.MILLISECONDS)) {
          if(!subscribeFuture.cancel(false)) {
            subscribeFuture.addListener(new FutureListener() {
              public void operationComplete(Future<RedissonLockEntry> future) throws Exception {
                if(subscribeFuture.isSuccess()) {
                  RedissonLock.this.unsubscribe(subscribeFuture, threadId);
                }
              }
            });
          }
          return false;
        } else {
          boolean var16;
          try {
            time -= System.currentTimeMillis() - current;
            if(time <= 0L) {
              boolean currentTime1 = false;
              return currentTime1;
            }
            do {
              long currentTime = System.currentTimeMillis();
              ttl = this.tryAcquire(leaseTime, unit);
              if(ttl == null) {
                var16 = true;
                return var16;
              }
              time -= System.currentTimeMillis() - currentTime;
              if(time <= 0L) {
                var16 = false;
                return var16;
              }
              currentTime = System.currentTimeMillis();
              if(ttl.longValue() >= 0L && ttl.longValue() < time) {
                this.getEntry(threadId).getLatch().tryAcquire(ttl.longValue(), TimeUnit.MILLISECONDS);
              } else {
                this.getEntry(threadId).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
              }
              time -= System.currentTimeMillis() - currentTime;
            } while(time > 0L);
            var16 = false;
          } finally {
            this.unsubscribe(subscribeFuture, threadId);
          }
          return var16;
        }
      }
    }
  }

Loop try 1 generally has the following methods:

The disadvantage of the while loop, one attempt after another, is that it creates a large number of invalid lock requests. Thread.sleep, increase the sleep time in the above while scheme to reduce the number of lock applications. The disadvantage is that the sleep time setting is difficult to control. Based on the amount of information, when the lock is occupied by other resources, the current thread subscrits to the release event of the lock. Once the lock is released, a message will be sent to inform the waiting lock to compete, effectively solving the invalid lock application situation. The core logic is this.getEntry(threadId).getLatch().tryAcquire, this.getEntry(threadId).

redisson rely on

Since redisson provides many ways for clients to manipulate redis, not just for locks, it relies on other frameworks, such as netty, that can be implemented by simply using locks.


Related articles: