Redis-Tutorial

Gute Links und Tutorials könnt ihr hier posten.
Antworten
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Redis ist ein in C geschriebener Key-Value-Store. Er verspricht Geschwindigkeit, garantiert allerdings weder Konsistenz noch Datensicherheit (nix ACID also). Er hält alle Daten im Hauptspeicher und schreibt periodisch einen Snapshot davon als Datei (wenn ich's richtig sehe, dann wird immer alles geschrieben, was sich bei einigen GB Daten dann doch schon ziehen kann).

Positiv zu vermerken ist, dass man mit dem Server per Textprotokoll per TCP/IP-Socket reden kann, wodurch es für eine Vielzahl von Programmiersprachen entsprechende APIs gibt, ohne das man mit irgendwelchen C-Bibliotheken fummeln muss.

Ich habe mir das Python-API mal näher angeschaut. Es könnte mehr Liebe vertragen. Ich hätte in vielen Details anders gebaut. Aber es funktioniert.

So verbindet man sich mit dem Server (auf dem selben Rechner), schreibt ein Datum und liest es wieder:

Code: Alles auswählen

    r = Redis()
    
    # speichere 42 unter dem Schlüssel 'answer'
    r.set("answer", 42)
    
    # speichere 43 unter 'answer' falls 'answer' noch nicht existiert
    # in meinem Fall passiert nichts, denn 'answer' existiert ja schon
    r.set("answer", 43, preserve=True)
    
    # speichere 44 unter 'answer' und liefere den alten Wert
    # ich hätte dem API hier einen eigenen Befehl spendiert
    answer = r.set("answer", 44, getset=True)
    
    # lies den gespeicherten Wert zu 'answer'
    # liefert `None` wenn der Schlüssel nicht existiert, bei mir 42
    answer = r.get("answer")
    
    # löscht einen Wert (und den Schlüssel)
    r.delete("answer")
    
    r.disconnect()
Das normale `set` liefert - warum auch immer - ein `'OK'` während das mit `preserve=True` 0 oder 1 wohl als Boolschen Wert liefert und das mit `getset=True` liefert den alten Wert. Die `delete`-Methode liefert wieder 0 oder 1 als Wahrheitswert.

Redis kennt keine Transaktionen, garantiert aber zumindest, dass die einzelnen Befehle atomar sind. `set(..., preserve=True)` bzw. `set(..., getset=True)` haben die notwendige test-und-set-Semantik, die man benötigt, um Locks und alle Arten von Synchronisationen zu realisieren.

Mit `mget` kann man übrigens gleich die Werte mehrerer Schlüssel erfragen. Nicht existente Schlüssel liefern `None`. Versucht man `None` als Wert zu schreiben, kommt übrigens `u'None'` zurück - das ist irgendwie ein Fehler. Ich sage daher erst mal, dass es wohl unmöglich ist, `None` in Redis abzulegen.

Mit `incr` bzw. `decr` kann man einen Zähler erhöhen. Ein optionales zweites Argument sagt um wie viel. Existiert der Schlüssel noch nicht, wird 0 angenommen. Auf diese Weise lassen sich einfach und effizient Zähler realisieren.

Man kann jedoch nicht nur einfache Strings oder Zahlen in Redis ablegen, sondern auch Listen (`list`) und Mengen (`set`). Dies ersetzt bzw. mildert die Notwendigkeit, JOINs wie bei relationalen Datenbanken einsetzen zu wollen. Soll ein Datensatz N andere Datensätze kennen, beschaffe ich mir (mit Hilfe eines Zählers) IDs und schiebe diese alle in eine Liste (oder eine Menge).

Code: Alles auswählen

    # ich will eine Person mit einem Namen speichern
    no = r.incr("uids:persons")
    r.set("people:%s:name" % no, "sma")
    
    # und diese Person soll Person #3 und #4 als Freunde haben
    # ich füge hinten an, müsste tail=True setzen, aber das API ist kaputt
    r.push("people:%s:friends" % no, 3, tail=False)
    r.push("people:%s:friends" % no, 4, tail=False)
    
    # jetzt kann ich nach den Freunden fragen
    friends = r.lrange("people:%s:friends" % no, 0, -1)
    
    # und die Namen holen
    names = r.mget(*["people:%s:name" % no for no in friends])
Mit `push` kann ich einen Schlüssel zu einer Liste machen (ein `get` führt dann zu einem kryptischen Fehler) und Werte vorne oder hinten anfügen. Das Python-API verwechselt hier leider vorne und hinten. Das ist ärgerlich. Mit `llen` kann ich die Länge der Liste bestimmen. Leider benutzt das Python-API genau die selben kryptischen Namen wie Redis. Daher muss ich `lrange` benutzen, um die Liste wieder zu lesen. Die `-1` aus meinem Beispiel bezeichnet das letzte Element.

Spielt die Reihenfolge keine Rolle, kann ich auch eine Menge benutzen. Hier beginnen die Methoden mit `s` und heißen auch leicht anders. Ich würde in Python die Namen harmonisieren. Wie auch immer, diese Listen- und Mengenoperationen machen Redis gegenüber anderen Key-Value-Stores interessant. So kann man etwa Teil- und Vereinigungsmengen bilden oder Werte sortieren.

Schließlich kann man wie bei memcached (und ähnlichen Systemen) auch Werte nur für eine bestimmte Zeit speichern. Mit `expire` kann man die Anzahl der Sekunden festlegen, nach der ein Wert (und der Schlüssel) gelöscht werden.

Ein bisschen kniffelig finde ich, wie man eine Liste umsortieren muss. Angenommen, ich speichere eine Liste Themen für ein Forum und möchte, dass das Thema mit dem neusten Beitrag oben ist:

Code: Alles auswählen

    # den folgenden Block kann immer nur einer durchlaufen
    with lock("locks:forum"):
        # ich hole mir alle IDs
        topics = r.lrange("forum:topics", 0, -1)
        # sortiere sie in Python um
        index = topics.index(no)
        t = topics[index]
        del topics[index]
        topics.insert(0, t)
        # der key könnte noch durch einen Absturz existeren, also weg damit
        r.delete("forum:topics:new")
        # jetzt kann ich alles zurückschreiben
        for t in topics:
            r.push("forum:topics:new", t)
        # mache die neue Liste zur echten Liste
        r.rename("forum:topics:new", "forum:topics")

    def lock(key, expire=1):
        # Sollte schon ein Lock existieren, ist ermöglicherweise von einem
        # Absturz übrig geblieben. Stelle sicher, dass er nach maximal einer
        # Sekunde vom Server weggeräumt wird.
        r.expire(key, expire)
        # Nun warte ich darauf, dass ich einen eigenen Lock setzen kann.
        while True:
            # Wenn der Schlüssel schon gesetzt ist, war das ein anderer Prozess
            # und ich warte. Wenn er noch nicht gesetzt war, setze ich ihn
            # atomar und bin fertig.
            if r.set(key, 1, preserve=True):
                # Sollte mein System abstürzen nachdem der Lock wurde und bevor
                # ich hin wieder löschen kann, sorgt der nächste Aufruf von
                # lock() dafür (siehe oben), dass er schließlich verschwindet.
                # existiert.
                return
            # Ansonsten warte ich und versuch's nochmal.
            timer.sleep(0.1)
        class Unlocker(object):
            def __enter__(self): pass
            def __exit__(self, exc, value, tb):
                # Lösche den Schlüssel. Andere können ihn nun neu setzen.
                r.delete(key)
        return Unlocker()
Der eigentliche Trick besteht darin, dass ich die neue Liste über ein `rename` atomar mit der alten Liste überschreibe. Leider kann ich eine Liste nicht einfach kopieren. Daher muss ich alle Elemente in einer `for`-Schleife schreiben. Damit niemand anderes in die Liste schreibt während ich sie ändere, muss ich einen Lock einsetzen. Gelingt es mir nicht, mit `set(..., preserve=True)` einen Wert zu setzen, gibt es schon einen Lock. Da theoretisch durch einen Absturz der Schlüssel gesetzt worden sein, nutze ich vorsorglich `expire`, damit mein System maximal eine Sekunde blockiert. Das "busy waiting" ist zwar nicht der Weisheit letzter Schluss, aber locking funktioniert - denke ich.

Redis scheint mir eine interessante Alternative zu einer traditionellen relationen Datenbank und zu anderen (einfacheren) Key-Value-Stores zu sein.

Update: Besseres Locking.

Stefan
Antworten