Redisson 分散ロック
前のアノテーション ベースのロックには、基本的な Redis 分散ロックであるロックの種類があり、ロックの実装は、 redisson コンポーネントの場合、この記事では redisson がロックを実装する方法について説明します。
バージョンが異なれば、ロック機構も異なります
は、最近リリースされた redisson のバージョン 3.2.3 を引用しています。バージョンが異なる場合があります。初期のバージョンでは、設定を完了するために単純な setnx、getset、およびその他の従来のコマンドを使用していたようですが、後に、redis がスクリプト Lua をサポートするため、実装の原則が変更されました。
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.2.3</version></dependency>
setnx は、デッドロックの問題を回避するために getset とトランザクションで完了する必要があります。新しいバージョンでは、トランザクションと複数の操作の使用を回避できます。 lua スクリプト redis コマンドの意味表現はより明確です。
RLockインターフェースの特徴
標準インターフェースLockを継承QySHTML5 Chinese Learning Network-HTML5 Pioneer Learningネットワーク には、ロック、ロック解除、トライロックなどの標準ロック インターフェイスのすべての機能があります。
QySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Network
標準インターフェイス Lock を拡張しますQySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Network多くのメソッドを拡張しました。主によく使用されるメソッドは次のとおりです: 強制ロック解除、有効期間ロックと一連の非同期メソッドを使用します。最初の 2 つの方法は主に、標準のロックによって発生する可能性のあるデッドロックの問題を解決するためのものです。たとえば、スレッドがロックを取得した後、そのスレッドが存在するマシンがクラッシュします。このとき、ロックを取得したスレッドはロックを正常に解放できず、ロックを待っている残りのスレッドが待機することになります。
QySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Network
リエントラントメカニズムQySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Network リエントラントの主な考慮事項は同じです。スレッドがロックを解放せずに再度ロック リソースを申請する場合、アプリケーション プロセスを実行する必要はなく、取得したロックを返し続けて再エントリ数を記録するだけで済みます。これは、ReentrantLock 関数と同様です。 jk。再エントリの数は、hincrby コマンドと組み合わせて使用されます。詳細なパラメーターは次のコードにあります。
QySHTML5 Chinese Learning Network - HTML5 Pioneer Learning Network
同じスレッドかどうかを確認するにはどうすればよいですか? QySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Network
redisson の解決策は、RedissonLock インスタンスの guid を現在のスレッドの ID に追加し、getLockName を通じてそれを返すことですQySHTML5 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; }
RLock がロックを取得するための 2 つのシナリオQySHTML5 Chinese Learning Network - HTML5 Pioneer Learning Network tryLock のソース コードは次のとおりです: tryAcquire メソッドは、 lock を実行し、ロック有効期間の残り時間を返します。空の場合は、他のスレッドによってロックが直接取得されて返されていないことを意味します。時間が取得された場合は、待機競合ロジックに入ります。
QySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Networkpublic 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 { //有竞争的后续看 } }
競合なし、直接ロックを取得しますQySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Networkまず、ロックを取得する理由を見てみましょうRedis は何をしているのですか? Redis モニターを使用して、バックグラウンドでの Redis の実行を監視できます。 @RequestLockable をメソッドに追加すると、実際に lock と lock を呼び出します。 以下は redis コマンドです:
QySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Network
LockQySHTML5 Chinese Learning Network-HTML5 Pioneer Learning Network Net redis の上位バージョンでは lua スクリプトがサポートされているため、redisson もサポートし、スクリプト モードを採用しています。lua スクリプトに詳しくない人は調べてください。 lua コマンドを実行するロジックは次のとおりです:
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(/'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)}); }
加锁的流程:
判断lock键是否存在,不存在直接调用hset存储当前线程信息并且设置过期时间,返回nil,告诉客户端直接获取到锁。
判断lock键是否存在,存在则将重入次数加1,并重新设置过期时间,返回nil,告诉客户端直接获取到锁。
被其它线程已经锁定,返回锁有效期的剩余时间,告诉客户端需要等待。
"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"
上面的lua脚本会转换成真正的redis命令,下面的是经过lua脚本运算之后实际执行的redis命令。
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"
解锁
解锁的流程看起来复杂些:
如果lock键不存在,发消息说锁已经可用
如果锁不是被当前线程锁定,则返回nil
由于支持可重入,在解锁时将重入次数需要减1
如果计算后的重入次数>0,则重新设置过期时间
如果计算后的重入次数<=0,则发消息说锁已经可用
"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"
无竞争情况下解锁redis命令:
主要是发送一个解锁的消息,以此唤醒等待队列中的线程重新竞争锁。
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 中国語 Web サイトの他の関連記事を参照してください。