Die neue Redis 5-Datenstruktur, Streams genannt, hat großes Interesse an der Community geweckt. Irgendwie werde ich mit denen sprechen, die Streams in der Produktion verwenden, und darüber schreiben. Aber jetzt möchte ich ein etwas anderes Thema betrachten. Es scheint mir, dass viele Leute Streams als eine Art surrealistisches Werkzeug zur Lösung schrecklich schwieriger Aufgaben betrachten. Diese Datenstruktur bietet zwar * auch * Messaging, aber es ist eine unglaubliche Vereinfachung anzunehmen, dass die Funktionalität von Redis Streams nur dadurch eingeschränkt wird.
Streams sind eine großartige Vorlage und ein „mentales Modell“, das mit großem Erfolg beim Systemdesign verwendet werden kann. In Wirklichkeit sind Streams jedoch wie die meisten Redis-Datenstrukturen eine allgemeinere Struktur und können für eine Reihe anderer Aufgaben verwendet werden. In diesem Artikel werden Streams als reine Datenstruktur dargestellt, wobei Blockierungsvorgänge, Empfängergruppen und alle anderen Messaging-Funktionen vollständig ignoriert werden.
Streams - Dies ist CSV für Steroide
Wenn Sie eine Reihe strukturierter Datenelemente aufzeichnen möchten und der Meinung sind, dass die Datenbank hier einen Überschuss darstellt, können Sie die Datei einfach im
append only
Modus öffnen und jede Zeile als CSV (Comma Separated Value) schreiben:
(open data.csv in append only) time=1553096724033,cpu_temp=23.4,load=2.3 time=1553096725029,cpu_temp=23.2,load=2.1
Es sieht einfach aus. Die Leute haben das schon vor langer Zeit getan und tun es immer noch: Es ist eine zuverlässige Vorlage, wenn Sie wissen, was was ist. Aber was wird das Äquivalent im Gedächtnis sein? Im Speicher wird eine wesentlich erweiterte Datenverarbeitung möglich, und viele Einschränkungen von CSV-Dateien werden automatisch entfernt, z.
- Es ist schwierig (ineffizient), Bereichsanforderungen zu erfüllen.
- Zu viele redundante Informationen: Jeder Datensatz hat fast dieselbe Zeit und die Felder werden dupliziert. Gleichzeitig wird durch das Löschen von Daten das Format weniger flexibel, wenn ich zu einem anderen Satz von Feldern wechseln möchte.
- Element-Offsets sind einfach Byte-Offsets in der Datei: Wenn wir die Struktur der Datei ändern, werden die Offsets falsch, sodass es kein wirkliches Konzept für einen primären Bezeichner gibt. Einträge können im Wesentlichen nicht eindeutig präsentiert werden.
- Ohne die Möglichkeit, Müll zu sammeln und das Protokoll neu zu schreiben, können Sie Einträge nicht löschen, sondern nur als ungültig markieren. Das Umschreiben von Protokollen ist normalerweise aus mehreren Gründen zum Kotzen. Es ist ratsam, dies zu vermeiden.
Gleichzeitig ist ein solches CSV-Protokoll auf seine Weise gut: Es gibt keine feste Struktur, Felder können sich ändern, es ist trivial, es zu generieren, und es ist recht kompakt. Die Idee mit Redis-Streams war, Tugenden zu bewahren, aber Einschränkungen zu überwinden. Das Ergebnis ist eine hybride Datenstruktur, die den von Redis sortierten Mengen sehr ähnlich ist: Sie sehen aus wie die grundlegende Datenstruktur, verwenden jedoch mehrere interne Darstellungen, um diesen Effekt zu erzielen.
Einführung in Threads (Sie können überspringen, wenn Sie bereits mit den Grundlagen vertraut sind)
Redis-Streams werden als deltakomprimierte Makroknoten dargestellt, die durch einen Basisbaum verbunden sind. Infolgedessen können Sie sehr schnell nach zufälligen Datensätzen suchen, Bereiche abrufen, alte Elemente löschen usw. Gleichzeitig ist die Benutzeroberfläche eines Programmierers einer CSV-Datei sehr ähnlich:
> XADD mystream * cpu-temp 23.4 load 2.3 "1553097561402-0" > XADD mystream * cpu-temp 23.2 load 2.1 "1553097568315-0"
Wie Sie dem Beispiel entnehmen können, generiert der XADD-Befehl automatisch die Kennung des Datensatzes und gibt diese zurück. Diese erhöht sich monoton und besteht aus zwei Teilen: <Zeit> - <Zähler>. Zeit in Millisekunden, und der Zähler wird für Datensätze mit derselben Zeit erhöht.
Die erste neue Abstraktion für die Idee einer CSV-Datei im
append only
Modus besteht darin, das Sternchen als ID-Argument für XADD zu verwenden: Auf diese Weise erhalten wir die Datensatzkennung kostenlos vom Server. Diese Kennung ist nicht nur nützlich, um ein bestimmtes Element im Stream anzugeben, sondern auch, wenn der Datensatz zum Stream hinzugefügt wurde. Mit XRANGE können Sie Bereichsabfragen ausführen oder einzelne Elemente abrufen:
> XRANGE mystream 1553097561402-0 1553097561402-0 1) 1) "1553097561402-0" 2) 1) "cpu-temp" 2) "23.4" 3) "load" 4) "2.3"
In diesem Fall habe ich dieselbe ID verwendet, um den Bereich zu starten und zu beenden, um einen Artikel zu identifizieren. Ich kann jedoch einen beliebigen Bereich und ein beliebiges COUNT-Argument verwenden, um die Anzahl der Ergebnisse zu begrenzen. Ebenso müssen keine vollständigen Bezeichner für einen Bereich angegeben werden. Ich kann einfach nur die Unix-Zeit verwenden, um Elemente in einem bestimmten Zeitbereich abzurufen:
> XRANGE mystream 1553097560000 1553097570000 1) 1) "1553097561402-0" 2) 1) "cpu-temp" 2) "23.4" 3) "load" 4) "2.3" 2) 1) "1553097568315-0" 2) 1) "cpu-temp" 2) "23.2" 3) "load" 4) "2.1"
Im Moment müssen keine weiteren API-Funktionen angezeigt werden. Hierzu gibt es eine Dokumentation. Konzentrieren wir uns zunächst nur auf dieses Verwendungsmuster: XADD zum Hinzufügen, XRANGE (und auch XREAD) zum Extrahieren von Bereichen (je nachdem, was Sie tun möchten), und lassen Sie uns sehen, warum Streams so leistungsfähig sind, dass sie als Datenstrukturen bezeichnet werden.
Wenn Sie mehr über Streams und APIs erfahren möchten, lesen Sie unbedingt das
Tutorial .
Tennisspieler
Vor ein paar Tagen simulierten ein Freund von mir, der mit dem Studium von Redis begann, und ich eine Anwendung, um lokale Tennisplätze, Spieler und Spiele zu verfolgen. Die Art und Weise, Spieler zu modellieren, ist offensichtlich. Der Spieler ist ein kleines Objekt, daher benötigen wir nur einen Hash mit Schlüsseln wie
player:<id>
. Dann werden Sie sofort erkennen, dass Sie eine Möglichkeit benötigen, Spiele in bestimmten Tennisclubs zu verfolgen. Wenn
player:1
und
player:2
untereinander gespielt und
player:1
gewonnen haben, können wir den folgenden Datensatz an den Stream senden:
> XADD club:1234.matches * player-a 1 player-b 2 winner 1 "1553254144387-0"
Eine so einfache Operation gibt uns:
- Eindeutige Übereinstimmungskennung: ID im Stream.
- Es ist nicht erforderlich, ein Objekt zur Übereinstimmungsidentifizierung zu erstellen.
- Freilandanfragen für Paging-Spiele oder das Ansehen von Spielen für ein bestimmtes Datum und eine bestimmte Uhrzeit.
Bevor Streams angezeigt werden, müssen wir eine nach Zeit sortierte Menge erstellen: Die Elemente der sortierten Menge sind Übereinstimmungskennungen, die in einem anderen Schlüssel als Hashwert gespeichert werden. Es ist nicht nur mehr Arbeit, sondern auch mehr Speicher. Viel, viel mehr Speicher (siehe unten).
Jetzt ist es unser Ziel zu zeigen, dass Redis-Streams eine Art sortierter Satz sind, der nur im
append only
Modus mit Schlüsseln nach Zeit festgelegt wird, wobei jedes Element ein kleiner Hash ist. Und in seiner Einfachheit ist dies eine echte Revolution im Kontext der Modellierung.
Die Erinnerung
Der obige Anwendungsfall ist nicht nur ein zusammenhängenderes Programmiermuster. Der Speicherverbrauch in Threads unterscheidet sich so stark vom alten Ansatz mit einem sortierten Satz + Hash für jedes Objekt, dass jetzt einige Dinge zu funktionieren beginnen, die zuvor überhaupt nicht implementiert werden konnten.
Hier finden Sie Statistiken zur Speichermenge für eine Million Übereinstimmungen in der zuvor dargestellten Konfiguration:
+ = 220 (242 RSS) = 16,8 (18.11 RSS)
Der Unterschied beträgt mehr als eine Größenordnung (nämlich das 13-fache). Dies bedeutet, dass Sie mit Aufgaben arbeiten können, deren Ausführung im Speicher zuvor zu teuer war. Jetzt sind sie durchaus lebensfähig. Die Magie besteht darin, Redis-Streams einzuführen: Makroknoten können mehrere Elemente enthalten, die sehr kompakt in einer Datenstruktur namens Listpack codiert sind. Diese Struktur kümmert sich beispielsweise darum, Ganzzahlen in binärer Form zu codieren, selbst wenn es sich um semantische Zeichenfolgen handelt. Außerdem wenden wir die Delta-Komprimierung an und komprimieren dieselben Felder. Es bleibt jedoch möglich, nach ID oder Zeit zu suchen, da solche Makroknoten in einem Basisbaum verknüpft sind, der ebenfalls mit Speicheroptimierung ausgelegt ist. Zusammen erklärt dies die wirtschaftliche Nutzung des Speichers, aber der interessante Teil ist, dass der Benutzer semantisch keine Implementierungsdetails sieht, die Threads so effizient machen.
Jetzt zählen wir. Wenn ich 1 Million Datensätze in ungefähr 18 MB Speicher speichern kann, kann ich 10 Millionen in 180 MB und 100 Millionen in 1,8 GB speichern. Mit nur 18 GB Speicher kann ich 1 Milliarde Artikel haben.
Zeitreihen
Es ist wichtig zu beachten, dass das obige Beispiel mit Tennisspielen semantisch * sehr * von der Verwendung von Redis-Streams für Zeitreihen abweicht. Ja, logischerweise registrieren wir immer noch eine Art Ereignis, aber es gibt einen grundlegenden Unterschied. Im ersten Fall protokollieren und erstellen wir Datensätze zum Rendern von Objekten. Und in der Zeitreihe messen wir einfach etwas, das außerhalb passiert und das Objekt nicht wirklich darstellt. Man kann sagen, dass diese Unterscheidung trivial ist, aber nicht. Es ist wichtig zu verstehen, dass Redis-Threads verwendet werden können, um kleine Objekte mit einer gemeinsamen Reihenfolge zu erstellen und solchen Objekten Bezeichner zuzuweisen.
Aber selbst die einfachste Art, Zeitreihen zu verwenden, ist offensichtlich ein großer Durchbruch, denn vor dem Aufkommen der Threads war Redis praktisch machtlos, hier etwas zu tun. Speichereigenschaften und Flexibilität von Streams sowie die Möglichkeit, begrenzte Streams (siehe XADD-Parameter) zu begrenzen, sind sehr wichtige Werkzeuge in den Händen des Entwicklers.
Schlussfolgerungen
Streams sind flexibel und bieten viele Anwendungsfälle, aber ich wollte einen sehr kurzen Artikel schreiben, um Beispiele und Speicherverbrauch klar darzustellen. Vielleicht war diese Verwendung von Streams für viele Leser offensichtlich. Die Gespräche mit Entwicklern in den letzten Monaten haben mich jedoch den Eindruck erweckt, dass viele eine starke Assoziation zwischen Streams und Streaming-Daten haben, als ob die Datenstruktur nur dort gut wäre. Es ist nicht so. :-)