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()
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])
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()
Redis scheint mir eine interessante Alternative zu einer traditionellen relationen Datenbank und zu anderen (einfacheren) Key-Value-Stores zu sein.
Update: Besseres Locking.
Stefan