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

Detailed explanation of the principle of distributed lock method implemented by redisson

巴扎黑
巴扎黑Original
2017-08-22 16:46:272884browse

Redisson distributed lockQySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Network

The previous annotation-based lock has one The kind of lock is a basic redis distributed lock. I implement the lock based on the RLock provided by the redisson component. This article will take a look at how redisson implements the lock. QySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Network

Different versions have different locking mechanismsQySHTML5 Chinese Learning Network- HTML5 Pioneer Learning Network

cited the recently released version 3.2.3 of redisson. Different versions may implement different lock mechanisms. Early versions seem to use simple setnx, getset and other conventional commands. The configuration is completed, but later the implementation principle was changed because redis supports script Lua. QySHTML5 Chinese Learning Network - HTML5 Pioneer Learning Network

<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 Because it supports Lua scripts, you can avoid using transactions and operating multiple redis commands, and the semantic expression is clearer. QySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Network

Characteristics of RLock interfaceQySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Network

Inherits the standard interface LockQySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Network

has a standard lock interface All features, such as lock, unlock, trylock, etc. QySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Network

Extended Standard Interface LockQySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Network

Extended many methods, the 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. QySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Network

Reentrant mechanismQySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Network

The implementation of each version is different. The main consideration for reentrancy is performance. If the same thread applies for lock resources again without releasing the lock, it does not need to go through the application process. It only needs to continue to return the lock that has been acquired. And just record the number of reentries, which 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. QySHTML5 Chinese Learning Network - HTML5 Pioneer Learning Network

How to determine whether it is the same thread? QySHTML5 Chinese Learning Network - HTML5 Pioneer Learning Network

redisson’s solution is to add a guid of the RedissonLock instance to the id of the current thread, Return by getLockNameQySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Network

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 obtain the lock QySHTML5 Chinese Learning Network - HTML5 Pioneer Learning Network

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

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, directly acquire the lockQySHTML5 Chinese Learning Network - HTML5 Pioneer Learning Network

Let’s first take a look at what redis is doing behind first acquiring the lock and releasing the lock. 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 are redis commands:

QySHTML5 Chinese Learning Network - HTML5 Pioneer Learning Network

Add LockQySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Network

Since higher versions of redis support lua scripts, redisson also supports it and adopts script mode , those who are not familiar with lua scripts can look it up. The logic of executing the lua command is as follows:

QySHTML5 Chinese Learning Network - HTML5 Pioneer Learning Network

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

加锁的流程:QySHTML5中文学习网 - HTML5先行者学习网

  1. 判断lock键是否存在,不存在直接调用hset存储当前线程信息并且设置过期时间,返回nil,告诉客户端直接获取到锁。

  2. 判断lock键是否存在,存在则将重入次数加1,并重新设置过期时间,返回nil,告诉客户端直接获取到锁。

  3. 被其它线程已经锁定,返回锁有效期的剩余时间,告诉客户端需要等待。

"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"

上面的lua脚本会转换成真正的redis命令,下面的是经过lua脚本运算之后实际执行的redis命令。QySHTML5中文学习网 - HTML5先行者学习网

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"

解锁QySHTML5中文学习网 - HTML5先行者学习网

解锁的流程看起来复杂些:QySHTML5中文学习网 - HTML5先行者学习网

  1. 如果lock键不存在,发消息说锁已经可用

  2. 如果锁不是被当前线程锁定,则返回nil

  3. 由于支持可重入,在解锁时将重入次数需要减1

  4. 如果计算后的重入次数>0,则重新设置过期时间

  5. 如果计算后的重入次数<=0,则发消息说锁已经可用

"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"

无竞争情况下解锁redis命令:QySHTML5中文学习网 - HTML5先行者学习网

主要是发送一个解锁的消息,以此唤醒等待队列中的线程重新竞争锁。QySHTML5中文学习网 - HTML5先行者学习网

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"

有竞争,等待QySHTML5中文学习网 - HTML5先行者学习网

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

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

循环尝试一般有如下几种方法:QySHTML5中文学习网 - HTML5先行者学习网

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

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

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

redisson依赖QySHTML5中文学习网 - HTML5先行者学习网

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

The above is the detailed content of Detailed explanation of the principle of distributed lock method implemented by redisson. For more information, please follow other related articles on the PHP Chinese website!

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