English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية
Verteilte Locks haben in der Regel drei Implementierungsmethoden:1.Datenbank optimistische Locks;2.Verteilte Locks basierend auf Redis;3.Verteilte Locks basierend auf ZooKeeper. In diesem Blog wird die zweite Methode vorgestellt, verteilte Locks basierend auf Redis zu implementieren. Obwohl es bereits viele Blogs über die Implementierung von Redis verteilten Locks gibt, gibt es jedoch eine Vielzahl von Problemen in ihren Implementierungen. Um die Schüler nicht zu irreführen, wird in diesem Blog detailliert erläutert, wie ein Redis verteilter Lock korrekt implementiert wird.
Verlässlichkeit
Zunächst müssen wir mindestens sicherstellen, dass die Implementierung des Locks gleichzeitig die folgenden vier Bedingungen erfüllt, um die Verfügbarkeit des verteilten Locks zu gewährleisten:
Mutual Exclusion. In jeder Zeit kann nur eine Client-Endstelle den Lock halten.
Kein Deadlock. Selbst wenn ein Client während der Lock-Haltung abstürzt und den Lock nicht aktiv löst, kann gewährleistet werden, dass andere Clients nachfolgend den Lock erlangen können.
Fehlertoleranz. Solange die meisten Redis-Knoten normal funktionieren, kann die Client-Endstelle Locken und Entlocken durchführen.
Lösen Sie das Problem mit demselben Werkzeug, das es verursacht hat. Locken und Entlocken müssen von derselben Client-Endstelle erfolgen, die Client-Endstelle kann nicht den Lock einer anderen Client-Endstelle lösen.
Code-Implementierung
Komponentenabhängigkeit
Zunächst müssen wir das Open-Source-Component Jedis durch Maven einführen und den folgenden Code in die Datei pom.xml hinzufügen:
Abhängigkeit> <groupId>redis.clients</GroupId> <artifactId>jedis</ArtifactId> <version>2.9.0</Version> </Abhängigkeit>
Lock-Code
Korrekter Stil
Reden ist Silber, Zeigen ist Gold. Zeige mir den Code, dann erkläre, warum dies so implementiert wurde:
public class RedisTool { private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; /** * Versuch, einen verteilten Lock zu erlangen * @param jedis Redis-Client * @param lockKey Schloss * @param requestId Anforderungsbezeichner * @param expireTime Zeitüberschreitung * @return Oberteil erfolgreich erlangt */ public static Boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } }
Man kann sehen, dass wir nur eine Zeile Code zum Locken verwenden: jedis.set(String key, String value, String nxxx, String expx, int time), diese set() Methode hat insgesamt fünf Parameter:
Der erste ist key, wir verwenden den Schlüssel als Lock, da der Schlüssel einzigartig ist.
Der zweite ist value, wir übergeben requestId, viele Schüler mögen es nicht verstehen, warum wir value verwenden, wenn ein Schlüssel als Lock ausreicht. Der Grund ist, dass wir in der oben genannten Zuverlässigkeit besprochen haben, dass ein Distributed Lock die vierte Bedingung erfüllen muss, d.h. 'Man muss, um den Lock zu lösen, denjenigen, der ihn gesetzt hat, finden'. Indem wir den Wert auf requestId setzen, wissen wir, welcher Request diesen Lock gesetzt hat, und können bei der Entsperrung darauf vertrauen. requestId kann mit der Methode UUID.randomUUID().toString() generiert werden.
Der dritte ist nxxx, dieser Parameter wird als NX gefüllt, was SETIFNOTEXIST bedeutet, d.h. wir führen die set-Operation durch, wenn der Schlüssel nicht existiert; wenn der Schlüssel bereits existiert, wird keine Aktion durchgeführt;
Der vierte ist expx, dieser Parameter wird als PX übergeben, was bedeutet, dass wir dem Schlüssel eine Ablaufzeit hinzufügen möchten, deren genaue Dauer durch den fünften Parameter bestimmt wird.
Der fünfte ist time, der mit dem vierten Parameter übereinstimmt und die Ablaufzeit des Schlüssels darstellt.
Zusammenfassend lässt sich sagen, dass die Ausführung der obigen set() Methode nur zwei Ergebnisse verursacht:1.Der aktuelle Lock ist nicht vorhanden (Schlüssel existiert nicht), dann wird der Lock-Operation durchgeführt und der Ablaufzeit des Locks eingestellt, wobei der Wert den Client, der den Lock ausführt, darstellt.2.Bestehender Lock existiert, keine Aktion durchzuführen.
Aufmerksamere Schüler werden feststellen, dass unser Lock-Code die drei Bedingungen unserer Zuverlässigkeit erfüllt, wie in der Beschreibung beschrieben. Zunächst wird der NX-Parameter in die Funktion set() eingebaut, um sicherzustellen, dass der Aufruf des Functions nicht erfolgreich ist, wenn bereits ein Schlüssel existiert, was bedeutet, dass nur ein Client den Lock halten kann und die Exklusivität erfüllt wird. Zweitrangig wird die Ablaufzeit des Locks eingestellt, sodass der Lock automatisch entsperrt wird (d.h. der Schlüssel wird gelöscht), wenn der Besitzer des Locks abstürzt und den Lock nicht entsperrt hat, was zu einem Deadlock führt. Schließlich wird der Wert auf requestId gesetzt, der die Identifikationsnummer der Client-Request für den Lock darstellt, sodass der Client bei der Entsperrung überprüfen kann, ob es sich um denselben Client handelt. Da wir nur die Single-Node-Deployment-Szenarien von Redis in Betracht ziehen, betrachten wir die Fehlerbehandlung vorläufig nicht.
Fehlerbeispiel1
Ein häufiger Fehlerbeispiel ist die Kombination von jedis.setnx() und jedis.expire() zur Implementierung des Locks, der Code sieht wie folgt aus:
public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) { long result = jedis.setnx(lockKey, requestId); if (result == 1) { // Wenn das Programm hier plötzlich abstürzt, kann die Ablaufzeit nicht gesetzt werden und es wird ein Deadlock auftreten jedis.expire(lockKey, expireTime); } }
Die Methode setnx() hat die Funktion SETIFNOTEXIST, die Methode expire() gibt dem Lock eine Ablaufzeit. Auf den ersten Blick scheint das Ergebnis der Methode set() ähnlich zu sein, aber da es sich um zwei Redis-Befehle handelt, die nicht atomar sind, kann es passieren, dass das Programm nach dem Ausführen von setnx() plötzlich abstürzt und der Lock keine Ablaufzeit gesetzt hat. Dies führt zu einem Deadlock. Warum jemand so implementiert, liegt daran, dass die alten Versionen von jedis die Methode set() mit mehreren Parametern nicht unterstützen.
Fehlerbeispiel2
Dieser Fehlerbeispiel ist schwer zu entdecken und die Implementierung ist auch komplex. Implementierungsansatz: Die Methode jedis.setnx() verwenden, um den Lock zu setzen, wobei der key der Lock und der value die Ablaufzeit des Locks ist. Der Ablaufprozess:1.Versucht den Lock mit der Methode setnx() zu setzen, wenn der aktuelle Lock nicht existiert, wird der Erfolg des Locks zurückgegeben.2.Wenn der Lock bereits existiert, wird die Ablaufzeit des Locks und der aktuelle Zeit verglichen. Wenn der Lock abgelaufen ist, wird eine neue Ablaufzeit gesetzt und der Erfolg des Locks zurückgegeben. Der Code sieht wie folgt aus:
public static Boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) { lang expires = System.currentTimeMillis() + expireTime; String expiresStr = String.valueOf(expires); // Wenn das aktuelle Schloss nicht existiert, wird der Erfolg des Lockings zurückgegeben if (jedis.setnx(lockKey, expiresStr) == 1) { return true; } // Wenn das Schloss existiert, holen Sie die Ablaufzeit des Schlosses String currentValueStr = jedis.get(lockKey); if (currentValueStr != null && long.parselong(currentValueStr) < System.currentTimeMillis()) { // Das Schloss ist abgelaufen, holen Sie die Ablaufzeit des vorherigen Schlosses und setzen Sie die aktuelle Ablaufzeit des Schlosses String oldValueStr = jedis.getSet(lockKey, expiresStr); if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { // Beachten Sie die Situation der Mehrthread-Parallelität, nur wenn der Wert des alten Wertes und der aktuelle Wert gleich sind, hat der Thread das Recht, den Lock zu erhalten return true; } } // In anderen Fällen wird immer der Erfolg des Lockings verweigert return false; }
Wo liegt das Problem in diesem Code?1.Da die Ablaufzeit vom Client selbst generiert wird, muss eine strenge Anforderung daran gestellt werden, dass die Zeit in der Verteilungsschicht synchronisiert ist.2.Wenn das Schloss abgelaufen ist und mehrere Clients gleichzeitig die Methode jedis.getSet() ausführen, kann obwohl letztendlich nur ein Client den Lock erhalten kann, die Ablaufzeit des Schlosses möglicherweise von anderen Clients überschrieben werden.3.Das Schloss hat keine Eigentümerkennung, d.h. jeder Client kann das Schloss entsperren.
Entschlüsselungscode
Korrekter Stil
Lassen Sie uns zunächst den Code präsentieren und dann langsam erklären, warum dies so implementiert wurde:
public class RedisTool { private static final long RELEASE_SUCCESS = 1L; /** * Freigabe des verteilten Schlosses * @param jedis Redis-Client * @param lockKey Schloss * @param requestId Anforderungsbezeichner * @return Oben ist der Erfolg der Freigabe */ public static Boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; } }
Man kann sehen, dass wir nur zwei Zeilen Code benötigen, um das Entsperrungsprozess zu erledigen! Die erste Zeile, wir schreiben einen einfachen Lua-Skriptcode, den wir zuletzt in 'Hackers & Painters' gesehen haben, und wir haben es nicht erwartet, dass wir es dieses Mal verwenden. Die zweite Zeile, wir geben den Lua-Code an die Methode jedis.eval() weiter und machen die Parameter KEYS[1】auf lockKey setzen, ARGV[1】auf requestId setzen. Die eval()-Methode gibt Lua-Code an den Redis-Server weiter.
Also was ist die Funktion dieses Lua-Codes? Tatsächlich sehr einfach, zuerst den Wert, der dem Schloss entspricht, abrufen, überprüfen, ob er mit requestId相等, wenn ja, das Schloss löschen (entsperren). Warum wird Lua-Sprache verwendet? Weil es sicherstellen muss, dass die oben genannten Operationen atomar sind. Über die Probleme, die nicht atomare Operationen verursachen können, kann man lesen in【Entsperrungscode-Fehlerbeispiel2】Daher warum kann die Ausführung von eval() die Atombildung sicherstellen? Dies ist auf die Eigenschaften von Redis zurückzuführen, hier ist ein Teil der Erklärung des eval-Befehls auf der offiziellen Website:
Kurz gesagt, wenn Lua-Code im eval-Befehl ausgeführt wird, wird Lua-Code als Befehl ausgeführt und Redis führt andere Befehle erst aus, wenn der eval-Befehl abgeschlossen ist.
Fehlerbeispiel1
Der häufigste Entsperrungscode ist die direkte Verwendung der Methode jedis.del() zum Löschen des Schlosses. Diese Methode entsperrt ohne vorher zu prüfen, ob der Besitzer des Schlosses, was zu dem führen kann, dass jeder Client jederzeit entsperrt, selbst wenn das Schloss nicht sein eigenes ist.
public static void wrongReleaseLock1(Jedis jedis, String lockKey) { jedis.del(lockKey); }
Fehlerbeispiel2
Diese Entsperrungscode scheint auf den ersten Blick ebenfalls in Ordnung zu sein, sogar ich war fast so implementiert, fast wie die richtige Haltung, der einzige Unterschied ist, dass es in zwei Aufträge aufgeteilt wird, der Code ist wie folgt:
public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) { // 判断加锁与解锁是否为同一个客户端 if (requestId.equals(jedis.get(lockKey))) { // 如果此时这把锁不属于这个客户端,则会误解锁 jedis.del(lockKey); } }
如代码注释,问题在于如果调用jedis.del()方法时,这把锁已经不属于当前客户端时,会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则会解除客户端B的锁。
总结
本文主要介绍了如何使用Java代码正确实现Redis分布式锁,对于加锁和解锁也分别给出了两个比较经典的错误示例。其实想要通过Redis实现分布式锁并不难,只要保证能满足可靠性中的四个条件。
分布式锁主要用在什么场景?需要在同步的地方,比如插入一条数据,需要事先检查数据库是否有类似的数据。多个请求同时插入时,可能会判断到数据库都返回没有类似的数据,则都可以加入。这时候需要进行同步处理,但是直接数据库锁表太耗时,所以采用Redis分布式锁,同时只能有一个线程去进行插入数据这个操作,其他的线程都等待。
以上就是本文关于Java语言描述Redis分布式锁的正确实现方式的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续阅读本站其他相关专题,如有不足之处,欢迎留言指出。感谢朋友们对本站的支持!
声明:本文内容来源于网络,归原作者所有。内容由互联网用户自发贡献并自行上传,本网站不拥有所有权,未进行人工编辑处理,也不承担相关法律责任。如果您发现涉嫌版权的内容,请发送邮件至:notice#oldtoolbag.com(在发送邮件时,请将#替换为@进行举报,并提供相关证据。一经查实,本站将立即删除涉嫌侵权的内容。)