Der Key-Value-Store Redis

Einf├╝hrung mit Beispielen

Dieser Artikel ist der erste in einer Folge von zw├Âlf Beitr├Ągen, die ich f├╝r den Adventskalender des deutschen Debianforums geschrieben habe. Die ersten vier stammen vom Adventskalender 2022. Weitere acht habe ich zum Adventskalender 2023 beigetragen. Diese Artikel m├Âchte ich hier mit leichten Anpassungen einem weiterem Publikum zug├Ąnglich machen. (Obwohl ich meine technischen Beitr├Ąge normalerweise auf Englisch schreibe, belasse ich diese im deutschsprachigen Original.)

Die Artikel setzen eine Installation von Debian 11 “Bullseye” oder 12 “Bookworm” voraus, k├Ânnen aber gr├Âsstenteils mit nur kleinen Anpassungen (Paketnamen) auch auf anderen Linux-Distributionen nachvollzogen werden.


In diesem Beitrag geht es um den Key-Value-Store Redis. Redis speichert Datenstrukturen, d.h. Daten unterschiedlicher Formen. Redis unterst├╝tzt eine Vielzahl von Datentypen, doch hier sollen zum Einstieg bloss folgende betrachtet werden:

Setup

Bevor wir mit diesen Datentypen arbeiten k├Ânnen, m├╝ssen wir aber Redis zuerst einmal installieren und starten. Hierf├╝r gibt es das Package redis:

# apt install -y redis 
# systemctl start redis-server.service

Neben dem Redis-Server wird auch redis-cli installiert, womit wir hier arbeiten werden:

$ redis-cli
127.0.0.1:6379> 

Mit dem PING-Befehl l├Ąsst sich die Konnektivit├Ąt ├╝berpr├╝fen (der Prompt wird ab hier mit > abgek├╝rzt):

> PING
PONG

Hilfe zu einem bestimmten Befehl gibt es mit dem HELP-Befehl:

> HELP PING
  PING [message]
  summary: Ping the server
  since: 1.0.0
  group: connection

Es gibt ├╝ber 400 Befehle, die auf der Redis-Befehls├╝bersicht sch├Ân dokumentiert sind. Eine Manpage sucht man vergebens, redis-cli verf├╝gt aber ├╝ber ein --help-Flag.

Einfache Werte

Redis kann man sich wie eine grosse Map vorstellen. Eine Map speichert Daten als Schl├╝ssel/Wert-Paare und kann somit als Verallgemeinerung des Arrays gesehen werden. (Bei einem Array sind die Schl├╝ssel Zahlen von 0 bis n-1, wobei n die Anzahl der Elemente ist; bei einer Map kann man beliebige Schl├╝ssel verwenden.)

Speichern wir also einige Werte mit SET ab:

> SET day 1
OK
> SET month December
OK
> SET year 2022
OK

Die Schl├╝ssel k├Ânnen mit KEYS [wildcard] (├Ąhnlich einem glob-Pattern) aufgelistet werden:

> KEYS *
1) "year"
2) "month"
3) "day"

Die Werte erh├Ąlt man mit GET zur├╝ck:

> GET day
"1"

Zwar ist der Wert als String abgespeichert, der INCR-Befehl kann ihn aber zu einer Zahl umwandeln und um 1 erh├Âht wieder abspeichern:

> INCR day
(integer) 2
> GET day
"2"
> GET month
"December"

Listen

Eine Liste ist kein Array, sondern eine verkettete Liste. Darum ist der Zugriff auf ein Element eine Operation der Ordnung O(n), das Anh├Ąngen vorne und hinten erfolgt jedoch in konstanter Zeit, sprich O(1). Legen wir also eine todo-Liste an:

> LPUSH todo work eat sleep
(integer) 3

Wir erhalten sogleich die Anzahl erstellter Listenelemente zur├╝ck. Auf die Elemente einer Liste kann mit dem LRANGE-Befehl zugegriffen werden, indem man einen Start- und einen End-Index (jeweils inklusive) definiert, wobei der Index bei 0 beginnt, und -1 f├╝r das letzte Element steht:

> LRANGE todo 0 -1
1) "sleep"
2) "eat"
3) "work"

Betrachten wir die todo-Liste wie eine Queue, wo wir von links Aufgaben hineinschieben, und von rechts her erledigen. Eine Aufgabe haben wir jedoch vergessen, und diese muss priorisiert, d.h. per RPUSH am rechten Ende eingef├╝gt werden!

> RPUSH todo "Tuerchen oeffnen"
(integer) 4
> LINDEX todo 3
1) "Tuerchen oeffnen"
> LRANGE todo 0 -1
1) "sleep"
2) "eat"
3) "work"
4) "Tuerchen oeffnen"

Da der Eintrag ein Leerzeichen enth├Ąlt, muss er mit Anf├╝hrungs- und Schlusszeichen umgeben sein. Mit LINDEX k├Ânnen wir direkt auf ein Element zugreifen.

Nun wollen wir aber auch eine Liste mit bereits erledigten Aufgaben anlegen:

> LPUSH done aufstehen
(integer) 1
> LRANGE done 0 -1
1) "aufstehen"

Da das T├╝rchen nun auch schon ge├Âffnet w├Ąre, k├Ânnen wir diese Aufgabe aus der todo-Liste entfernen und der done-Liste hinzuf├╝gen. Damit uns hier nicht ein Schuft dazwischenfunkt, machen wir daf├╝r eine Transaktion mit MULTI:

> MULTI
> RPOP done
QUEUED
> DISCARD
OK

Das ging schief: Statt auf todo wurde hier RPOP auf done verwendet. Zum Gl├╝ck kann man die Transaktion mit DISCARD r├╝ckg├Ąngig machen. Dieses mal aber richtig, und dann mit EXEC auch tats├Ąchlich ausf├╝hren:

> MULTI
> RPOP todo
QUEUED
> LPUSH done "Tuerchen oeffnen"
QUEUED
> EXEC
1) "Tuerchen oeffnen"
2) (integer) 2

Die beiden Ergebnisse erhalten wir erst ganz am Schluss. Das Ergebnis stimmt aber:

> LRANGE todo 0 -1
1) "sleep"
2) "eat"
3) "work"
> LRANGE done 0 -1
1) "Tuerchen oeffnen"
2) "aufstehen"

Praktisch ist das nicht. Einfacher ginge es mit dem RPOPLPUSH-Befehl, der das letzte Element von der ersten Liste entfernt und der zweiten List als erstes Element hinzuf├╝gt:

> RPOPLPUSH todo done
"work"

Dann w├Ąre das mit der Arbeit ja f├╝r heute auch schon erledigtÔÇŽ Aber wir wollen noch Sets betrachten.

Sets

Ein Set ist eine Menge, d.h. eine Sammlung von Werten, in der jeder Wert eindeutig ist. Besch├Ąftigen wir uns mit Zahlenreihen, genauer mit der 2er- und der 3er-Reihe. Mit SADD k├Ânnen wir einem Set Werte hinzuf├╝gen. Das Set wird bei Bedarf gleich erstellt:

> SADD two-times-table 2 4 6 8 10 12 14 16 18 20
(integer) 10
> SADD three-times-table 3 6 9 12 15 18 21 24 27 30
(integer) 10

Die grundlegenden Mengenoperationen Schnittmenge, Vereinigungsmenge und Differenzmenge k├Ânnen mit den Befehlen SINTER, SUNION und SDIFF erstellt werden:

> SUNION two-times-table three-times-table
 1) "2"
 2) "3"
 3) "4"
 4) "6"
 5) "8"
 6) "9"
 7) "10"
 8) "12"
 9) "14"
10) "15"
11) "16"
12) "18"
13) "20"
14) "21"
15) "24"
16) "27"
17) "30"
> SINTER two-times-table three-times-table
1) "6"
2) "12"
3) "18"
> SDIFF three-times-table two-times-table
1) "3"
2) "9"
3) "15"
4) "21"
5) "24"
6) "27"
7) "30"

Die drei Befehle gibt es auch mit dem Pr├Ąfix STORE, womit man die Mengen auch gleich ablegen kann.

Hashes

Mit dem Hash k├Ânnen Werte mit Unterwerten abgespeichert werden. Im Gegensatz zu einer Liste speichert man damit eher heterogene Werte ab, z.B. Eigenschaften von Forenteilnehmern.

Mit HSET kann ein neuer hash erstellt werden; der Befehl erwartet paarweise Schl├╝ssel/Wert-Paare als weitere Argumente:

> HSET paedubucher posts 624 location Schweiz license GFDL
(integer) 3

Mit HGETALL k├Ânnen dann alle Schl├╝ssel und Werte von einem Hash zur├╝ckgegeben werden:

> HGETALL paedubucher
HGETALL paedubucher
1) "posts"
2) "624"
3) "location"
4) "Schweiz"
5) "license"
6) "GFDL"

Nun mag es so aussehen, dass hier einfach eine Ansammlung von Werten gespeichert wird. Wir haben es aber tats├Ąchlich mit Schl├╝ssel/Wert-Paaren zu tun, wie die HGET-Funktion das demonstriert:

> HGET paedubucher posts
"624"

Unter dem Schl├╝ssel posts verbirgt sich also eine Zahl, die wir aufgrund dieses Beitrags erh├Âhen wollen. Hierzu nehmen wir den HINCRBY-Befehl:

> HINCRBY paedubucher posts 1
(integer) 625

Bonus: Export

Redis ist kein Datenfriedhof, sondern interagiert sehr gut mit der Aussenwelt. Neben zahlreichen Sprachbindungen gibt es auch die M├Âglichkeit, Daten im CSV- oder JSON-Format zu exportieren:

$ redis-cli --csv HGETALL paedubucher
"posts","625","location","Schweiz","license","GFDL"

$ redis-cli --json HGETALL paedubucher
{"posts":"625","location":"Schweiz","license":"GFDL"}

Wie findet Ihr das? Habt Ihr schon eigene Erfahrungen mit Redis gemacht? K├Ânnt Ihr das einsetzen? Wie konfiguriert Ihr Redis? Verwendet Ihr es als Cache, oder speichert er die Daten im RDB- oder AOF-Format persistent ab? Habt Ihr schon einmal die Lua-Integration verwendet, oder sonstige Sprachanbindungen? Verwendet Ihr Redis gar als Message Queue?