Home  >  Article  >  Java  >  Detailed explanation of the principle of distributed lock implemented by redisson

Detailed explanation of the principle of distributed lock implemented by redisson

黄舟
黄舟Original
2017-03-07 10:26:382810browse

This article will introduce in detail the principle of distributed lock implemented by redisson. It has a very good reference value. Let’s take a look at it with the editor.

Redisson distributed lock

One of the previous annotation-based locks is the basic redis Distributed lock, I implement the lock based on RLock provided by the redisson component. This article will take a look at how redisson implements the lock.

The mechanisms for implementing locks are different in different versions.

The recently released version 3.2.3 of redisson quoted is different. Different versions may implement locks. The mechanism is different. The early version seemed to use simple setnx, getset and other conventional commands to complete the configuration. However, in the later period, the implementation principle was changed because redis supported script Lua.

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

setnx needs to be completed with getset and transactions, so as to better avoid deadlock problems, and the new version can avoid the use of transactions because it supports lua scripts As well as operating multiple redis commands, the semantic expression is clearer.

Characteristics of RLock interface

Inherits the standard interface Lock

It has all the features of the standard lock interface, such as lock, unlock, trylock, etc.

Extended standard interface Lock

Extends many methods, the most commonly used ones are: forced lock release, lock with validity period, and a set of Asynchronous methods. The first two methods are mainly to solve the deadlock problem that may be caused by standard lock. For example, after a thread acquires a lock, the machine where the thread is located crashes. At this time, the thread that acquired the lock cannot release the lock normally, causing the remaining threads waiting for the lock to wait.

Reentrant mechanism

The implementation of each version is different. The main consideration for reentrancy is performance. When the same thread does not release the lock If you apply for a lock resource again, you do not need to go through the application process. You only need to continue to return the acquired lock and record the number of reentries. This is similar to the ReentrantLock function in jdk. The number of reentries is used in conjunction with the hincrby command. The detailed parameters are in the code below.

How to determine if it is the same thread?

redisson’s solution is to add a guid of the RedissonLock instance to the id of the current thread, and return

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

Two scenarios for RLock to acquire a lock

Here is the source code of tryLock: the tryAcquire method is to apply Lock and return the remaining time of the lock validity period. If it is empty, it means that the lock has not been directly acquired and returned by other threads. If the time is acquired, the waiting competition logic will be entered.

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 {
   //有竞争的后续看
  }
 }

No competition, acquire the lock directly

Let’s first look at acquiring the lock first and What is redis doing behind the lock release? You can use the redis monitor to monitor the execution of redis in the background. When we add @RequestLockable to the method, we actually call lock and unlock. The following is the redis command:

Lock

Due to the high The version of redis supports lua scripts, so redisson also supports it and adopts script mode. If you are not familiar with lua scripts, you can look it up. The logic of 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(\&#39;exists\&#39;, KEYS[1]) == 0) then redis.call(\&#39;hset\&#39;, KEYS[1], ARGV[2], 1); 
    redis.call(\&#39;pexpire\&#39;, KEYS[1], ARGV[1]); return nil; end; 
    if (redis.call(\&#39;hexists\&#39;, KEYS[1], ARGV[2]) == 1) then redis.call(\&#39;hincrby\&#39;, KEYS[1], ARGV[2], 1); 
    redis.call(\&#39;pexpire\&#39;, KEYS[1], ARGV[1]); return nil; end; 
    return redis.call(\&#39;pttl\&#39;, KEYS[1]);", 
    Collections.singletonList(this.getName()), new Object[]{Long.valueOf(this.internalLockLeaseTime), this.getLockName(threadId)});
  }

Locking process:

  1. Determine whether the lock key exists. If it does not exist, directly call hset to store the current thread information and set the expiration time. Return nil to tell the client to obtain the lock directly.

  2. Determine whether the lock key exists. If it exists, increase the number of reentries by 1, reset the expiration time, and return nil to tell the client to obtain the lock directly.

  3. Has been locked by other threads, returns the remaining time of the lock validity period, and tells the client that it needs to wait.

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

The above Lua script will be converted into a real redis command. The following is the actual redis command 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"

Unlocking

Unlocking process Looks more complicated:

  1. If the lock key does not exist, send a message saying that the lock is available

  2. If the lock is not held by the current thread If locked, return nil

  3. Since reentrancy is supported, the number of reentries needs to be reduced by 1 when unlocking

  4. If the calculated reentry If the number of re-entries is >0, the expiration time will be reset.

  5. If the calculated number of re-entries is <=0, a message will be sent saying that the lock is available

"EVAL" 
"if (redis.call(&#39;exists&#39;, KEYS[1]) == 0) then
 redis.call(&#39;publish&#39;, KEYS[2], ARGV[1]);
 return 1; end;
if (redis.call(&#39;hexists&#39;, KEYS[1], ARGV[3]) == 0) then 
return nil;end; 
local counter = redis.call(&#39;hincrby&#39;, KEYS[1], ARGV[3], -1); 
if (counter > 0) then redis.call(&#39;pexpire&#39;, KEYS[1], ARGV[2]); return 0; 
else redis.call(&#39;del&#39;, KEYS[1]); redis.call(&#39;publish&#39;, 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"

Unlock the redis command without competition:

Mainly sends an unlock message to wake up the threads waiting in the queue to compete for the lock again .

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"

有竞争,等待

有竞争的情况在redis端的lua脚本是相同的,只是不同的条件执行不同的redis命令,复杂的在redisson的源码上。当通过tryAcquire发现锁被其它线程申请时,需要进入等待竞争逻辑中。

  • this.await返回false,说明等待时间已经超出获取锁最大等待时间,取消订阅并返回获取锁失败

  • this.await返回true,进入循环尝试获取锁。

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 {
      //重点是这段
      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;
        }
      }
    }
  }

循环尝试一般有如下几种方法:

  • while循环,一次接着一次的尝试,这个方法的缺点是会造成大量无效的锁申请。

  • Thread.sleep,在上面的while方案中增加睡眠时间以降低锁申请次数,缺点是这个睡眠的时间设置比较难控制。

  • 基于信息量,当锁被其它资源占用时,当前线程订阅锁的释放事件,一旦锁释放会发消息通知待等待的锁进行竞争,有效的解决了无效的锁申请情况。核心逻辑是this.getEntry(threadId).getLatch().tryAcquire,this.getEntry(threadId).getLatch()返回的是一个信号量,有兴趣可以再研究研究。

redisson依赖

由于redisson不光是针对锁,提供了很多客户端操作redis的方法,所以会依赖一些其它的框架,比如netty,如果只是简单的使用锁也可以自己去实现。

 以上就是redisson实现分布式锁原理详解的内容,更多相关内容请关注PHP中文网(www.php.cn)!


Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn