Redis Best Practices, Teil 2

Der zweite Teil des Redis Best Practices-Übersetzungszyklus von Redis Labs behandelt Interaktionsmuster und Datenspeichermuster.

Der erste Teil ist hier .

Interaktionsmuster


Redis kann nicht nur als traditionelles DBMS fungieren, sondern seine Strukturen und Befehle können auch zum Austausch von Nachrichten zwischen Mikrodiensten oder Prozessen verwendet werden. Durch die weit verbreitete Verwendung von Redis-Clients, die Geschwindigkeit und Effizienz des Servers und des Protokolls sowie die integrierten klassischen Strukturen können Sie Ihre eigenen Workflows und Ereignismechanismen erstellen. In diesem Kapitel werden die folgenden Themen behandelt:

  • Warteschlange der Ereignisse;
  • Blockieren mit Redlock;
  • Pub / Sub;
  • verteilte Ereignisse.

Ereigniswarteschlange


Listen in Redis sind geordnete Zeilenlisten, die verknüpften Listen, mit denen Sie möglicherweise vertraut sind, sehr ähnlich sind. Das Hinzufügen eines Werts zu einer Liste (Push) und das Löschen eines Werts aus einer Liste (Pop) sind sehr einfache Vorgänge. Wie Sie sich vorstellen können, ist dies eine sehr gute Struktur für die Verwaltung einer Warteschlange: Fügen Sie Elemente am Anfang hinzu und lesen Sie sie vom Ende (FIFO). Redis bietet außerdem zusätzliche Funktionen, die dieses Muster effizienter, zuverlässiger und benutzerfreundlicher machen.

Listen enthalten eine Teilmenge von Befehlen, mit denen Sie das Blockierungsverhalten ausführen können. Der Begriff "Blockieren" bezieht sich auf eine Verbindung mit nur einem Client. Tatsächlich erlauben diese Befehle dem Client nichts zu tun, bis ein Wert in der Liste angezeigt wird oder bis das Zeitlimit abläuft. Dadurch müssen Sie Redis nicht mehr abfragen und auf das Ergebnis warten. Da der Client nichts tun kann, während er einen Wert erwartet, benötigen wir zwei offene Clients, um dies zu veranschaulichen:
#Kunde 1Client 2
1
> BRPOP my-q 0
[Wert erwarten]
2
> LPUSH my-q hello
(integer) 1
1) "my-q"
2) "hello"
[Client entsperrt, bereit, Befehle anzunehmen]
3
> BRPOP my-q 0
[Wert erwarten]

In diesem Beispiel sehen wir in Schritt 1, dass der blockierte Client nichts sofort zurückgibt, da er nichts enthält. Das letzte Argument ist die Wartezeit. Hier bedeutet 0 ewige Erwartung. In der zweiten Zeile wird ein Wert in my-q eingegeben und der erste Client verlässt sofort den Blockierungsstatus. In der dritten Zeile wird BRPOP erneut aufgerufen (Sie können dies in einer Schleife in der Anwendung tun), und der Client wartet auch auf den nächsten Wert. Durch Drücken von „Strg + C“ können Sie die Sperre aufheben und den Client verlassen.

Lassen Sie uns das Beispiel umkehren und sehen, wie BRPOP mit einer nicht leeren Liste funktioniert:
#Kunde 1Client 2
1
> LPUSH my-q hello
(integer) 1
2
> LPUSH my-q hej
(integer) 2
3
> LPUSH my-q bonjour
(integer) 3
4
> BRPOP my-q 0
1) "my-q"
2) "hello"
5
> BRPOP my-q 0
1) "my-q"
2) "hej"
6
> BRPOP my-q 0
1) "my-q"
2) "bonjour"
7
> BRPOP my-q 0
[Wert erwarten]

In den Schritten 1-3 fügen wir der Liste 3 Werte hinzu und sehen, dass die Antwort wächst und die Anzahl der Elemente in der Liste angibt. Schritt 4 gibt trotz des Aufrufs von BRPOP den Wert sofort zurück. Dies liegt daran, dass das Blockierungsverhalten nur auftritt, wenn sich keine Werte in der Warteschlange befinden. In den Schritten 5 bis 6 wird dieselbe sofortige Antwort angezeigt, da dies für jedes Element in der Warteschlange erfolgt. In Schritt 7 findet BRPOP nichts in der Warteschlange und blockiert den Client, bis etwas hinzugefügt wird.

Oft stellen Warteschlangen einige Arbeiten dar, die in einem anderen Prozess (Worker) ausgeführt werden müssen. Bei dieser Art von Arbeitslast ist es wichtig, dass die Arbeit nicht verschwindet, wenn der Arbeiter während der Ausführung aus irgendeinem Grund fällt. Redis unterstützt diese Art von Warteschlange. Verwenden Sie dazu den Befehl BRPOPLPUSH anstelle von BRPOP. Sie erwartet einen Wert in einer Liste und fügt ihn in eine andere Liste ein, sobald er dort angezeigt wird. Dies geschieht atomar, so dass es für zwei Arbeiter unmöglich ist, den gleichen Wert zu ändern. Mal sehen, wie es funktioniert:
#Kunde 1Client 2
1
> LINDEX worker-q 0
(nil)
2[Wenn das Ergebnis nicht Null ist, verarbeiten Sie es irgendwie und fahren Sie mit Schritt 4 fort.]
3
> LREM worker-q -1 [   1]
(integer) 1
[zurück zu Schritt 1]
4
> BRPOPLPUSH my-q worker-q 0
[Wert erwarten]
5
> LPUSH my-q hello
"hello"
[Client entsperrt, bereit, Befehle anzunehmen]
6[Hallo behandeln]
7
> LREM worker-q -1 hello
(integer) 1
8[zurück zu Schritt 1]

In den Schritten 1-2 tun wir nichts, da worker-q leer ist. Wenn etwas zurückgekehrt ist, verarbeiten wir es und löschen es. Kehren wir dann zu Schritt 1 zurück, um zu überprüfen, ob etwas in die Warteschlange gelangt ist. Daher löschen wir zuerst die Warteschlange des Arbeiters und führen die vorhandene Arbeit aus. In Schritt 4 warten wir, bis der Wert in my-q erscheint , und wenn dies der Fall ist, wird er atomar an worker-q übertragen . Dann verarbeiten wir irgendwie "Hallo" , danach löschen wir es aus Arbeiter-q und kehren zu Schritt 1 zurück. Wenn der Prozess in Schritt 6 stirbt, bleibt der Wert immer noch in Arbeiter-q . Nach dem Neustart des Prozesses löschen wir sofort alles, was in Schritt 7 nicht gelöscht wurde.

Dieses Muster verringert die Wahrscheinlichkeit eines Arbeitsplatzverlusts erheblich, jedoch nur, wenn der Arbeitnehmer zwischen den Schritten 2 und 3 oder 5 und 6 stirbt, was unwahrscheinlich ist. Best Practice berücksichtigt dies jedoch in der Logik des Arbeitnehmers.

Mit Redlock sperren


Manchmal ist es im System erforderlich, eine Ressource zu blockieren. Dies kann erforderlich sein, um wichtige Änderungen vorzunehmen, die in einem Wettbewerbsumfeld nicht gelöst werden können. Blockierungsziele:

  • Erlauben Sie einem und nur einem Mitarbeiter, die Ressource zu erfassen.
  • in der Lage sein, das Sperrobjekt zuverlässig freizugeben;
  • Sperren Sie die Ressource nicht fest (muss nach einer bestimmten Zeit entsperrt werden).

Redis ist eine gute Option für die Implementierung des Blockierens, da es ein einfaches schlüsselbasiertes Datenmodell hat und jeder Shard Single-Threaded und ziemlich schnell ist. Es gibt eine hervorragende Lock-Implementierung mit Redis namens Redlock.
Redlock-Clients sind für fast jede Sprache verfügbar. Es ist jedoch wichtig zu wissen, wie Redlock funktioniert, um es sicher und effektiv verwenden zu können.

Zunächst müssen Sie verstehen, dass Redlock für die Ausführung auf mindestens 3 Computern mit unabhängigen Redis-Instanzen ausgelegt ist. Dadurch wird der einzelne Fehlerpunkt in Ihrem Sperrmechanismus beseitigt, der zu einem Deadlock aller Ressourcen führen kann. Ein weiterer zu verstehender Punkt ist, dass die Uhren an den Maschinen zwar nicht zu 100% synchronisiert sein sollten, aber auf die gleiche Weise funktionieren sollten - die Zeit bewegt sich mit der gleichen Geschwindigkeit: eine Sekunde an der Maschine und die gleiche wie eine Sekunde an Maschine B.

Das Festlegen eines Sperrobjekts mit Redlock beginnt mit dem Abrufen eines Zeitstempels mit Millisekundengenauigkeit. Sie müssen auch die Sperrzeit im Voraus angeben. Anschließend wird das blockierende Objekt festgelegt, indem der Schlüssel mit einem zufälligen Wert festgelegt (SET) wird (nur wenn dieser Schlüssel noch nicht vorhanden ist) und das Zeitlimit für den Schlüssel festgelegt wird. Dies wird für jede unabhängige Instanz wiederholt. Wenn die Instanz ausfällt, wird sie sofort übersprungen. Wenn das Sperrobjekt in den meisten Fällen vor Ablauf des Zeitlimits erfolgreich installiert wurde, wird es als erfasst betrachtet. Die Zeit zum Installieren oder Aktualisieren des Sperrobjekts ist die Zeit, die zum Erreichen des Sperrstatus benötigt wird, abzüglich der vordefinierten Sperrzeit. Entsperren Sie im Falle eines Fehlers oder einer Zeitüberschreitung alle Instanzen und versuchen Sie es erneut.

Um ein Sperrobjekt freizugeben, ist es besser, ein Lua-Skript zu verwenden, das prüft, ob der erwartete Zufallswert im Schlüsselsatz enthalten ist. Wenn dies der Fall ist, können Sie es löschen. Andernfalls ist es besser, die Schlüssel zu belassen, da es sich möglicherweise um neuere Sperrobjekte handelt.

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

Der Redlock-Prozess bietet gute Garantien und das Fehlen eines einzelnen Fehlerpunkts, sodass Sie absolut sicher sein können, dass einzelne Sperrobjekte verteilt werden und keine gegenseitigen Sperren auftreten.

Pub / Sub


Neben der Datenspeicherung kann Redis auch als Pub / Sub-Plattform (Herausgeber / Abonnent) verwendet werden. In diesem Muster kann ein Herausgeber Nachrichten an eine beliebige Anzahl von Kanalabonnenten senden. Hierbei handelt es sich um Nachrichten, die auf dem Prinzip „Schießen und Vergessen“ basieren. Wenn die Nachricht freigegeben wird und der Abonnent nicht vorhanden ist, verschwindet die Nachricht ohne die Möglichkeit einer Wiederherstellung.
Durch das Abonnieren des Kanals wechselt der Client in den Abonnentenmodus und kann keine Befehle mehr aufrufen - er wird schreibgeschützt. Der Verlag hat keine derartigen Einschränkungen.

Sie können mehr als einen Kanal abonnieren. Wir beginnen mit dem Abonnieren der beiden Wetter- und Sportkanäle mit dem Befehl SUBSCRIBE:

> SUBSCRIBE weather sports
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "weather"
3) (integer) 1
1) "subscribe"
2) "sports"
3) (integer) 2

In einem separaten Client (z. B. einem anderen Terminalfenster) können wir Nachrichten in jedem dieser Kanäle mit dem Befehl PUBLISH veröffentlichen:

> PUBLISH sports oilers/7:leafs/1
(integer) 1

Das erste Argument ist der Name des Kanals, das zweite ist die Nachricht. Die Nachricht kann eine beliebige sein, in diesem Fall handelt es sich um ein codiertes Konto im Spiel. Der Befehl gibt die Anzahl der Clients zurück, an die die Nachricht gesendet wird. Im Abonnenten-Client sehen wir sofort die Nachricht:

1) "message"
2) "sports"
3) "oilers/7:leafs/1"

Die Antwort enthält drei Elemente: einen Hinweis darauf, dass es sich um eine Nachricht, einen Abonnementkanal und tatsächlich eine Nachricht handelt. Der Client kehrt unmittelbar nach dem Empfang zum Abhören des Kanals zurück.

Wenn wir zum Verlag zurückkehren, können wir eine weitere Nachricht posten:

> PUBLISH weather snow/-4c
(integer) 1

Im Abonnenten sehen wir das gleiche Format, aber mit einem anderen Kanal mit der Nachricht:

1) "message"
2) "weather"
3) "snow/-4c"

Senden wir eine Nachricht an einen Kanal, in dem es keine Abonnenten gibt:

> PUBLISH currency CADUSD/0.787
(integer) 0

Da niemand den Währungskanal abhört , lautet die Antwort 0. Diese Nachricht ist nicht mehr vorhanden, und Kunden, die diesen Kanal abonniert haben, erhalten keine Benachrichtigung über diese Nachricht - sie wurde gesendet und vergessen.

Redis abonniert nicht nur einen einzelnen Kanal, sondern auch das Abonnieren von Kanälen per Maske. Die Glob-Maske wird an den Befehl PSUBSCRIBE übergeben:

> PSUBSCRIBE sports:*

Der Kunde erhält Nachrichten von allen Kanälen, beginnend mit Sport : . Rufen Sie in einem anderen Client die folgenden Befehle auf:

> PUBLISH sports:hockey oilers/7:leafs/1
(integer) 1
> PUBLISH sports:basketball raptors/33:pacers/7
(integer) 1
> PUBLISH weather:edmonton snow/-4c
(integer) 0

Bitte beachten Sie, dass die ersten beiden Teams 1 zurückgeben, während das letzte 0 zurückgibt. Obwohl wir Sport nicht direkt abonniert haben : Hockey oder Sport: Basketball , erhält der Kunde Nachrichten über ein Abonnement per Maske. Im Client-Abonnenten-Fenster können wir sehen, dass es nur Ergebnisse für Kanäle gibt, die mit der Maske übereinstimmen.

1) "pmessage"
2) "sports:*"
3) "sports:hockey"
4) "oilers/7:leafs/1"
1) "pmessage"
2) "sports:*"
3) "sports:basketball"
4) "raptors/33:pacers/7"

Diese Ausgabe unterscheidet sich geringfügig von der Ausgabe des Befehls SUBSCRIBE, da sie die Maske selbst sowie den tatsächlichen Namen des Kanals enthält.

Verteilte Ereignisse


Das Pub / Sub-Messaging-Schema von Redis kann erweitert werden, um interessante verteilte Ereignisse zu erstellen. Angenommen, wir haben eine Struktur, die in einer Hash-Tabelle gespeichert ist, aber wir möchten Clients nur aktualisieren, wenn ein einzelnes Feld den vom Abonnenten festgelegten numerischen Wert überschreitet. Wir werden die Kanäle per Maske anhören und den Hash- Status extrahieren . In diesem Beispiel interessiert uns update_status mit den Werten 5-9.

> PSUBSCRIBE update_status:[5-9]
1) "psubscribe"
2) "update_status:[5-9]"
3) (integer) 1
...

Um den Wert status / error_level zu ändern , benötigen wir zwei Befehle, die nacheinander oder im MULTI / EXEC-Block ausgeführt werden können. Der erste Befehl legt die Ebene fest und der zweite veröffentlicht eine Benachrichtigung mit dem im Kanal selbst codierten Wert.

> HSET status error_level 5
(integer) 1
> PUBLISH update_status:5 0
(integer) 1

Im ersten Fenster sehen wir, dass die Nachricht empfangen wurde. Danach können Sie zu einem anderen Client wechseln und den Befehl HGETALL aufrufen:

...
1) "pmessage"
2) "update_status:[5-9]"
3) "update_status:5"
4) "0"

> HGETALL status
1) "error_level"
2) "5"

Wir können diese Methode auch verwenden, um die lokale Variable eines längeren Prozesses zu aktualisieren. Auf diese Weise können mehrere Instanzen desselben Prozesses Daten in Echtzeit austauschen.

Warum ist dieses Muster besser als Pub / Sub? Wenn der Prozess neu gestartet wird, kann er einfach den gesamten Status abrufen und mit dem Abhören beginnen. Änderungen werden zwischen einer beliebigen Anzahl von Prozessen synchronisiert.

Datenspeichermuster


Es gibt verschiedene Muster zum Speichern strukturierter Daten in Redis. In diesem Kapitel werden wir Folgendes betrachten:

  • Datenspeicherung in JSON;
  • Lagerhäuser.

JSON-Datenspeicherung


Es gibt verschiedene Optionen zum Speichern von JSON-Daten in Redis. Die häufigste Form besteht darin, das Objekt im Voraus zu serialisieren und unter einem speziellen Schlüssel zu speichern:

> SET car "{\"colour\":\"blue\",\"make\":\"saab\",\"model\":93,\"features\":[\"powerlocks\",\"moonroof\"]}"
OK
> GET car
"{\"colour\":\"blue\",\"make\":\"saab\",\"model\":93,\"features\":[\"powerlocks\",\"moonroof\"]}"

Es scheint einfach auszusehen, hat aber einige sehr schwerwiegende Nachteile:

  • Für die Serialisierung sind Client-Computerressourcen zum Lesen und Schreiben erforderlich.
  • Das JSON-Format erhöht die Datengröße.
  • Redis hat nur eine indirekte Möglichkeit, Daten in JSON zu verarbeiten.

Die ersten paar Punkte mögen bei kleinen Datenmengen vernachlässigbar sein, aber die Kosten steigen, wenn die Daten wachsen. Der dritte Punkt ist jedoch der kritischste.

Vor Redis 4.0 bestand die einzige Möglichkeit, mit JSON in Redis zu arbeiten, darin, ein Lua-Skript im cjson-Modul zu verwenden. Dies löste das Problem teilweise, obwohl es immer noch ein Engpass blieb und zusätzliche Probleme beim Erlernen von Lua verursachte. Darüber hinaus haben viele Anwendungen einfach die gesamte JSON-Zeichenfolge empfangen, deserialisiert, mit den Daten gearbeitet, zurück serialisiert und erneut gespeichert. Dies ist ein Antimuster. Auf diese Weise besteht ein großes Risiko, Daten zu verlieren.

#Anwendungsinstanz Nr. 1Anwendungsinstanz Nr. 2
1
> GET my-car
2[deserialisieren, Maschinenfarbe ändern und erneut serialisieren]
> GET my-car
3
> SET my-car

[neuer Wert von Instanz 1]
[deserialisieren, Maschinenmodell ändern und erneut serialisieren]
4
> SET my-car

[neuer Wert von Instanz 2]
5
> GET my-car

Das Ergebnis in Zeile 5 zeigt nur Änderungen an Instanz 2 an, und die Farbänderung durch Instanz 1 geht verloren.

Redis Version 4.0 und höher bietet die Möglichkeit, Module zu verwenden. ReJSON ist ein Modul, das einen speziellen Datentyp und Befehle für die direkte Interaktion mit diesem bereitstellt. ReJSON speichert die Daten im Binärformat, wodurch die Größe der gespeicherten Daten verringert wird und ein schnellerer Zugriff auf Elemente ermöglicht wird, ohne Zeit für die De- / Serialisierung aufzuwenden.

Um ReJSON verwenden zu können, müssen Sie es auf einem Redis-Server installieren oder in Redis Enterprise aktivieren.

Das vorherige Beispiel mit ReJSON würde folgendermaßen aussehen:

#Anwendungsinstanz Nr. 1Anwendungsinstanz Nr. 2
1
> JSON.SET car2 . '{"colour": "blue",  "make":"saab", "model":93,  "features": ["powerlocks",  "moonroof"]}‘
OK
2
> JSON.SET car2 colour '"red"'
OK
3
> JSON.SET car2 model '95'
OK
> JSON.GET car2 .
"{\"colour\":\"red",\"make\":\"saab\",\"model\":95,\"features\":[\"powerlocks\",\"moonroof\"]}"

ReJSON bietet eine sicherere, schnellere und intuitivere Möglichkeit, mit JSON-Daten in Redis zu arbeiten, insbesondere in Fällen, in denen atomare Änderungen an verschachtelten Elementen erforderlich sind.

Objektspeicherung


Auf den ersten Blick scheint der Standarddatentyp "Hash-Tabelle" von Redis einem JSON-Objekt oder einem anderen Typ sehr ähnlich zu sein. Es ist viel einfacher, Felder entweder als Zeichenfolge oder als Zahl zu erstellen und verschachtelte Strukturen zu verhindern. Nachdem Sie den „Pfad“ zu jedem Feld berechnet haben, können Sie das Objekt jedoch „reduzieren“ und in der Redis-Hash-Tabelle speichern.

{
    "colour": "blue",
    "make": "saab",
    "model": {
        "trim": "aero",
        "name": 93
    },
    "features": ["powerlocks", "moonroof"]
}

Mit JSONPath (XPath für JSON) können wir jedes Element auf derselben Ebene der Hash-Tabelle darstellen:

> HSET car3 colour blue
> HSET car3 make saab
> HSET car3 model.trim aero
> HSET car3 model.name 93
> HSET car3 features[0] powerlocks
> HSET car3 features[1] moonroof

Aus Gründen der Übersichtlichkeit werden die Befehle separat aufgeführt, es können jedoch viele Parameter an HSET übergeben werden.

Jetzt können Sie das gesamte Objekt oder sein einzelnes Feld anfordern:

> HGETALL car3
 1) "colour"
 2) "blue"
 3) "make"
 4) "saab"
 5) "model.trim"
 6) "aero"
 7) "model.name"
 8) "93"
 9) "features[0]"
10) "powerlocks"
11) "features[1]"
12) "moonroof"

> HGET car3 model.trim
"aero"

Dies bietet zwar eine schnelle und nützliche Möglichkeit, ein in Redis gespeichertes Objekt abzurufen, hat jedoch folgende Nachteile:

  • In verschiedenen Sprachen und Bibliotheken kann die Implementierung von JSONPath unterschiedlich sein, was zu Inkompatibilität führt. In diesem Fall lohnt es sich, die Daten mit einem Tool zu serialisieren und zu deserialisieren.
  • Array-Unterstützung:
    • spärliche Arrays können problematisch sein;
    • Es ist unmöglich, viele Operationen auszuführen, z. B. das Einfügen eines Elements in die Mitte eines Arrays.

  • Unnötiger Ressourcenverbrauch in JSONPath-Schlüsseln.

Dieses Muster ist so ziemlich das gleiche wie bei ReJSON. Wenn ReJSON verfügbar ist, ist es in den meisten Fällen besser, es zu verwenden. Das Speichern von Objekten auf die oben beschriebene Weise hat jedoch einen Vorteil gegenüber ReJSON: die Integration in das Redis SORT-Team. Dieser Befehl ist jedoch rechenintensiv und stellt ein separates komplexes Thema dar, das über den Rahmen dieses Musters hinausgeht.

Der nächste abschließende Teil behandelt Zeitreihenmuster, Geschwindigkeitsbegrenzungsmuster, Bloom-Filtermuster, Zähler und die Verwendung von Lua in Redis.

PS Ich habe versucht, den Text dieser Artikel in „barbarischem“ Englisch so weit wie möglich ins Russische zu übersetzen. Wenn Sie jedoch der Meinung sind, dass die Idee irgendwo unverständlich oder falsch ist, korrigieren Sie mich in den Kommentaren.

Source: https://habr.com/ru/post/undefined/


All Articles