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:
- Strings: Eine Zeichenkette, die aber auch als Zahl interpretiert werden kann.
- Listen: Mehrere Einträge in einer verketteten(!) Liste.
- Sets: Mengen mit eindeutigen Elementen.
- Hashes: Mehrere Einträge als Schlüssel/Wert-Paare.
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?