Heim  >  Artikel  >  Datenbank  >  Einführung in die korrekte Implementierung der verteilten Redis-Sperre

Einführung in die korrekte Implementierung der verteilten Redis-Sperre

尚
nach vorne
2019-12-11 17:20:402730Durchsuche

Einführung in die korrekte Implementierung der verteilten Redis-Sperre

Es gibt im Allgemeinen drei Möglichkeiten, verteilte Sperren zu implementieren:

1. Redis-basierte verteilte Sperre; >

3. Verteilte Sperre basierend auf ZooKeeper.

In diesem Artikel wird die zweite Möglichkeit zur Implementierung verteilter Sperren basierend auf Redis vorgestellt. Obwohl es im Internet verschiedene Blogs gibt, die die Implementierung verteilter Redis-Sperren vorstellen, weisen ihre Implementierungen verschiedene Probleme auf. Um Irreführungen der Leser zu vermeiden, wird in diesem Blog ausführlich beschrieben, wie verteilte Redis-Sperren korrekt implementiert werden.

Zuverlässigkeit

Um sicherzustellen, dass verteilte Sperren verfügbar sind, müssen wir zunächst zumindest sicherstellen, dass die Sperrimplementierung den Anforderungen entspricht vier Bedingungen gleichzeitig erfüllen:

1. Gegenseitige Exklusivität. Es kann immer nur ein Client die Sperre halten.

2. Es kommt zu keinem Deadlock. Selbst wenn ein Client abstürzt, während er die Sperre hält, ohne sie aktiv zu entsperren, ist gewährleistet, dass andere Clients ihn anschließend sperren können.

3. Fehlertolerant. Solange die meisten Redis-Knoten normal laufen, kann der Client sperren und entsperren.

4. Um die Glocke zu lösen, müssen Sie die Glocke binden. Das Sperren und Entsperren muss vom selben Client durchgeführt werden. Der Client selbst kann die von anderen hinzugefügte Sperre nicht entsperren.

Code-Implementierung

Komponentenabhängigkeiten

Zuerst müssen wir die Open-Source-Komponenten von Jedis durch einführen Maven. Fügen Sie der pom.xml-Datei den folgenden Code hinzu:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

Sperrcode

Korrekte Haltung

Talk is günstig, zeig mir den Code. Zeigen Sie zuerst den Code und erklären Sie dann langsam, warum er auf diese Weise implementiert wird:

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

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    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;

    }

}

Wie Sie sehen, benötigen wir zum Sperren nur eine Codezeile: jedis.set(String key, String value, String nxxx , String expx, int time ), diese set()-Methode hat insgesamt fünf formale Parameter:

Der erste ist der Schlüssel. Wir verwenden den Schlüssel als Sperre, da der Schlüssel eindeutig ist.

Der zweite Punkt ist der Wert, den wir weitergeben. Viele Kinder verstehen möglicherweise nicht, dass ein Schlüssel als Schloss dient. Der Grund dafür ist, dass, wenn wir oben über Zuverlässigkeit gesprochen haben, die verteilte Sperre die vierte Bedingung erfüllen muss, um die Glocke zu entsperren, und die Person, die die Glocke hält, die Person sein muss, die die Glocke gebunden hat. Durch Zuweisen des Werts zu requestId wissen wir, welche Anfrage vorliegt Beim Entsperren können Sie dann eine Basis hinzufügen. requestId kann mit der Methode UUID.randomUUID().toString() generiert werden.

Der dritte ist nxxx, was SET IF NOT EXIST bedeutet, das heißt, wenn der Schlüssel nicht existiert, führen wir die Set-Operation aus, nein Operation wird ausgeführt;

Der vierte Parameter ist PX, was bedeutet, dass wir diesem Schlüssel eine Ablaufeinstellung hinzufügen möchten. Die spezifische Zeit wird durch den fünften Parameter bestimmt.

Der fünfte Parameter ist die Zeit, die dem vierten Parameter entspricht und die Ablaufzeit des Schlüssels darstellt.

Im Allgemeinen führt die Ausführung der oben genannten set()-Methode nur zu zwei Ergebnissen: 1. Derzeit ist keine Sperre vorhanden (der Schlüssel ist nicht vorhanden). Führen Sie dann den Sperrvorgang aus und legen Sie einen Gültigkeitszeitraum für fest lock und value repräsentiert den gesperrten Client. 2. Die Sperre ist bereits vorhanden, es wird kein Vorgang ausgeführt.

Aufmerksame Kinder werden feststellen, dass unser Sperrcode die drei in unserer Zuverlässigkeit beschriebenen Bedingungen erfüllt:

1. Zuerst fügt set() NX-Parameter hinzu, die sicherstellen können, dass der Schlüssel bereits vorhanden ist Die Funktion wird nicht erfolgreich aufgerufen, d. h. nur ein Client kann die Sperre halten, wodurch der gegenseitige Ausschluss erfüllt ist.

2. Zweitens, da wir eine Ablaufzeit für das Schloss festlegen, wird das Schloss automatisch entriegelt (d. h. der Schlüssel wird gelöscht), auch wenn der Schlosshalter anschließend abstürzt bis zur Ablaufzeit. Es kommt zu keinem Deadlock.

3. Da wir schließlich requestId einen Wert zuweisen, der die Identifikation der gesperrten Clientanforderung darstellt, kann beim Entsperren des Clients überprüft werden, ob es sich um denselben Client handelt. Da wir nur das Szenario der eigenständigen Redis-Bereitstellung betrachten, werden wir die Fehlertoleranz vorerst nicht berücksichtigen.

Fehlerbeispiel 1

Ein häufigeres Fehlerbeispiel ist die Verwendung einer Kombination aus jedis.setnx() und jedis.expire() zum Implementieren der Sperre wie folgt:

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {

    Long result = jedis.setnx(lockKey, requestId);
    if (result == 1) {
        // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
        jedis.expire(lockKey, expireTime);
    }

}

Die Methode setnx() fungiert als SET IF NOT EXIST, und die Methode Expire() fügt der Sperre eine Ablaufzeit hinzu. Auf den ersten Blick scheint das Ergebnis das gleiche zu sein wie bei der vorherigen set()-Methode. Da es sich jedoch um zwei Redis-Befehle handelt, sind sie nicht atomar. Wenn das Programm nach der Ausführung von setnx() plötzlich abstürzt, wird die Sperre nicht aktiviert Ablaufzeit eingestellt. Dann kommt es zu einem Deadlock. Der Grund, warum einige Leute dies im Internet implementieren, liegt darin, dass niedrigere Versionen von Jedis die Multiparameter-Set()-Methode nicht unterstützen.

Fehlerbeispiel 2

public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {

    long expires = System.currentTimeMillis() + expireTime;
    String expiresStr = String.valueOf(expires);

    // 如果当前锁不存在,返回加锁成功
    if (jedis.setnx(lockKey, expiresStr) == 1) {
        return true;
    }

    // 如果锁存在,获取锁的过期时间
    String currentValueStr = jedis.get(lockKey);
    if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
        // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
        String oldValueStr = jedis.getSet(lockKey, expiresStr);
        if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
            // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
            return true;
        }
    }
        
    // 其他情况,一律返回加锁失败
    return false;

}
Diese Art von Fehlerbeispiel ist schwieriger zu finden und die Implementierung ist auch komplizierter. Implementierungsidee: Verwenden Sie den Befehl jedis.setnx(), um die Sperre zu implementieren, wobei der Schlüssel die Sperre und der Wert die Ablaufzeit der Sperre ist.

Ausführungsprozess:

1. Versuchen Sie, über die setnx()-Methode zu sperren. Wenn die aktuelle Sperre nicht vorhanden ist, geben Sie die Sperre erfolgreich zurück.

2. Wenn die Sperre bereits vorhanden ist, ermitteln Sie die Ablaufzeit der Sperre und vergleichen Sie sie mit der aktuellen Zeit. Wenn die Sperre abgelaufen ist, legen Sie die neue Ablaufzeit fest und geben Sie die Sperre erfolgreich zurück. Der Code lautet wie folgt:

Was ist also das Problem mit diesem Code?

1、由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。 

2、当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。

3、锁不具备拥有者标识,即任何客户端都可以解锁。

解锁代码

正确姿势

还是先展示代码,再带大家慢慢解释为什么这样实现:

public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        String script = "if redis.call(&#39;get&#39;, KEYS[1]) == ARGV[1] then return redis.call(&#39;del&#39;, 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;

    }

}

可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。关于非原子性会带来什么问题,可以阅读【解锁代码-错误示例2】 。

那么为什么执行eval()方法可以确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:

简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

错误示例1

最常见的解锁代码就是直接使用jedis.del()方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。

public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
    jedis.del(lockKey);
}

错误示例2

这种解锁代码乍一看也是没问题,甚至我之前也差点这样实现,与正确姿势差不多,唯一区别的是分成两条命令去执行,代码如下:

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的锁给解除了。

更多redis知识请关注redis数据库教程栏目。

Das obige ist der detaillierte Inhalt vonEinführung in die korrekte Implementierung der verteilten Redis-Sperre. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:cnblogs.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen