Cet article présentera en détail le principe du verrouillage distribué implémenté par redisson. Il a une très bonne valeur de référence. Jetons un coup d'œil avec l'éditeur ci-dessous
Verrou distribué Redisson
Le verrou basé sur les annotations précédent a une sorte de verrou qui est Basic redis Verrou distribué, j'implémente le verrou basé sur RLock fourni par le composant redisson. Cet article examinera comment redisson implémente le verrou.
Les mécanismes d'implémentation des verrous sont différents selon les versions
La version 3.2.3 récemment publiée de Redisson citée, différentes versions peuvent implémenter des verrous Le mécanisme est différent. La première version semblait utiliser de simples commandes setnx, getset et autres commandes conventionnelles pour compléter la configuration. Dans la période ultérieure, le principe de mise en œuvre a été modifié car Redis prenait en charge le script Lua.
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.2.3</version> </dependency>
setnx doit être complété avec getset et transactions, afin de mieux éviter les problèmes de blocage, et la nouvelle version peut éviter cela car elle prend en charge Lua scripts En utilisant des transactions et en exécutant plusieurs commandes redis, l'expression sémantique est plus claire.
Caractéristiques de l'interface RLock
Hérite de l'interface standard Lock
Il possède toutes les fonctionnalités de l'interface de verrouillage standard, telles que le verrouillage, le déverrouillage, le trylock, etc.
Étend l'interface standard Lock
étend de nombreuses méthodes Les plus couramment utilisées sont : le déverrouillage forcé, le verrouillage avec période de validité et un. ensemble de méthodes asynchrones. Les deux premières méthodes visent principalement à résoudre le problème de blocage qui peut être causé par le verrouillage standard. Par exemple, après qu'un thread a acquis un verrou, la machine sur laquelle se trouve le thread tombe en panne. À ce moment-là, le thread qui a acquis le verrou ne peut pas libérer le verrou normalement, ce qui fait attendre les threads restants en attente du verrou.
Mécanisme réentrant
Il existe des différences dans la mise en œuvre de chaque version. La principale considération pour la réentrée est la performance lorsque le même thread ne fonctionne pas. libérer le verrou Si vous demandez à nouveau une ressource de verrouillage, vous n'avez pas besoin de suivre le processus de demande. Il vous suffit de continuer à renvoyer le verrou acquis et d'enregistrer le nombre de réentrées, ce qui est similaire à la fonction ReentrantLock dans jdk. Le nombre de réentrées est utilisé conjointement avec la commande hincrby. Les paramètres détaillés sont dans le code ci-dessous.
Comment déterminer s'il s'agit du même fil ?
La solution de Redisson consiste à ajouter un GUID de l'instance RedissonLock à l'identifiant du thread actuel et à renvoyer
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; }
Deux scénarios pour que RLock acquière le verrou
Voici le code source de tryLock : méthode tryAcquire Il s'agit de demander le verrou et de renvoyer le temps restant de la période de validité du verrou. S'il est vide, cela signifie que le verrou n'a pas été directement acquis et renvoyé par d'autres threads. la logique de compétition en attente sera introduite.
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 { //有竞争的后续看 } }
Pas de concurrence, acquérez la serrure directement
Regardez le premier acquisition Que fait Redis derrière le verrouillage et la libération du verrouillage Vous pouvez utiliser le moniteur Redis pour surveiller l'exécution de Redis en arrière-plan ? Lorsque nous ajoutons @RequestLockable à la méthode, nous appelons en fait lock et unlock. Voici les commandes redis :
Lock
En raison du. high La version de redis prend en charge les scripts Lua, donc Redisson le prend également en charge et adopte le mode script. Ceux qui ne sont pas familiers avec les scripts Lua peuvent le rechercher. La logique d'exécution de la commande lua est la suivante :
<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)}); }
Le processus de verrouillage :
Déterminez si la clé de verrouillage existe. Si elle n'existe pas, appelez directement hset pour stocker les informations du thread actuel et définissez le délai d'expiration pour indiquer au client d'obtenir le verrou directement.
Déterminez si la clé de verrouillage existe. Si elle existe, augmentez le nombre de réentrées de 1, réinitialisez le délai d'expiration et renvoyez zéro pour indiquer au client d'obtenir le verrou directement.
a été verrouillé par d'autres threads, renvoie le temps restant de la période de validité du verrouillage et indique au client qu'il doit attendre.
"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"
Le script lua ci-dessus sera converti en une véritable commande redis, et la suivante est en fait exécutée après le lua commande redis d’opération de script.
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"
Déverrouillage
Processus de déverrouillageCela semble plus compliqué :
Si la clé de verrouillage n'existe pas, envoyez un message indiquant que le verrou est disponible
Si le verrou n'est pas verrouillé Si le thread actuel est verrouillé, nul est renvoyé
La réentrée étant prise en charge, le nombre de réentrants doit être réduit de 1 lors du déverrouillage
Si calculé Si le nombre calculé de réentrées est >0, alors le délai d'expiration est réinitialisé
Si le nombre calculé de réentrées est <=0, puis un message est envoyé indiquant que le verrou est disponible
"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"
Déverrouillez la commande redis sans concurrence :
Envoie principalement un message de déverrouillage pour réveiller la file d'attente. Le thread est à nouveau en compétition pour le verrouillage.
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)!