Redis
 sql >> Datenbank >  >> NoSQL >> Redis

So verwenden Sie Redis mit Python

In diesem Tutorial erfahren Sie, wie Sie Python mit Redis (ausgesprochen RED-iss oder vielleicht REE-diss oder Red-DEES, je nachdem, wen Sie fragen) verwenden, einem blitzschnellen In-Memory-Schlüsselwertspeicher kann für alles von A bis Z verwendet werden. Hier ist was Sieben Datenbanken in sieben Wochen , ein beliebtes Buch über Datenbanken, hat Folgendes über Redis zu sagen:

Es ist nicht nur einfach zu bedienen; es ist eine Freude. Wenn eine API UX für Programmierer ist, dann sollte Redis neben dem Mac Cube im Museum of Modern Art stehen.

Und wenn es um Geschwindigkeit geht, ist Redis kaum zu schlagen. Lesevorgänge sind schnell und Schreibvorgänge sind sogar noch schneller und verarbeiten mehr als 100.000 SET Operationen pro Sekunde durch einige Benchmarks. (Quelle)

Fasziniert? Dieses Tutorial wurde für Python-Programmierer entwickelt, die möglicherweise keine oder nur wenig Redis-Erfahrung haben. Wir werden zwei Tools gleichzeitig angehen und sowohl Redis selbst als auch eine seiner Python-Client-Bibliotheken, redis-py, vorstellen .

redis-py (die Sie einfach als redis importieren ) ist einer von vielen Python-Clients für Redis, aber es hat den Unterschied, dass es von den Redis-Entwicklern selbst als „derzeit der richtige Weg für Python“ in Rechnung gestellt wird. Damit können Sie Redis-Befehle von Python aus aufrufen und im Gegenzug vertraute Python-Objekte zurückerhalten.

In diesem Tutorial behandeln Sie :

  • Installieren von Redis aus der Quelle und Verstehen des Zwecks der resultierenden Binärdateien
  • Erlernen eines mundgerechten Stücks von Redis selbst, einschließlich Syntax, Protokoll und Design
  • redis-py beherrschen und gleichzeitig Einblicke in die Implementierung des Redis-Protokolls erhalten
  • Einrichten und Kommunizieren mit einer Amazon ElastiCache Redis-Serverinstanz

Kostenloser Download: Holen Sie sich ein Beispielkapitel aus Python Tricks:The Book, das Ihnen die Best Practices von Python mit einfachen Beispielen zeigt, die Sie sofort anwenden können, um schöneren + Pythonic-Code zu schreiben.


Installieren von Redis aus der Quelle

Wie mein Ururgroßvater sagte, nichts baut Grit auf wie die Installation von der Quelle. Dieser Abschnitt führt Sie durch das Herunterladen, Erstellen und Installieren von Redis. Ich verspreche, dass das kein bisschen weh tun wird!

Hinweis :Dieser Abschnitt ist auf die Installation unter Mac OS X oder Linux ausgerichtet. Wenn Sie Windows verwenden, gibt es einen Microsoft-Fork von Redis, der als Windows-Dienst installiert werden kann. Es genügt zu sagen, dass Redis als Programm am bequemsten auf einer Linux-Box läuft und dass die Einrichtung und Verwendung unter Windows schwierig sein kann.

Laden Sie zuerst den Redis-Quellcode als Tarball herunter:

$ redisurl="http://download.redis.io/redis-stable.tar.gz"
$ curl -s -o redis-stable.tar.gz $redisurl

Als nächstes wechseln Sie zu root und extrahieren Sie den Quellcode des Archivs nach /usr/local/lib/ :

$ sudo su root
$ mkdir -p /usr/local/lib/
$ chmod a+w /usr/local/lib/
$ tar -C /usr/local/lib/ -xzf redis-stable.tar.gz

Optional können Sie jetzt das Archiv selbst entfernen:

$ rm redis-stable.tar.gz

Dadurch erhalten Sie ein Quellcode-Repository unter /usr/local/lib/redis-stable/ . Redis ist in C geschrieben, daher müssen Sie mit make kompilieren, verknüpfen und installieren Dienstprogramm:

$ cd /usr/local/lib/redis-stable/
$ make && make install

Mit make install führt zwei Aktionen aus:

  1. Das erste make Befehl kompiliert und verlinkt den Quellcode.

  2. Die make install part nimmt die Binärdateien und kopiert sie nach /usr/local/bin/ sodass Sie sie von überall ausführen können (vorausgesetzt, dass /usr/local/bin/ befindet sich in PATH ).

Hier sind alle bisherigen Schritte:

$ redisurl="http://download.redis.io/redis-stable.tar.gz"
$ curl -s -o redis-stable.tar.gz $redisurl
$ sudo su root
$ mkdir -p /usr/local/lib/
$ chmod a+w /usr/local/lib/
$ tar -C /usr/local/lib/ -xzf redis-stable.tar.gz
$ rm redis-stable.tar.gz
$ cd /usr/local/lib/redis-stable/
$ make && make install

Nehmen Sie sich an dieser Stelle einen Moment Zeit, um zu bestätigen, dass sich Redis in Ihrem PATH befindet und prüfen Sie die Version:

$ redis-cli --version
redis-cli 5.0.3

Wenn Ihre Shell redis-cli nicht finden kann , stellen Sie sicher, dass /usr/local/bin/ befindet sich auf Ihrem PATH Umgebungsvariable und fügen Sie sie hinzu, wenn nicht.

Zusätzlich zu redis-cli , make install führt tatsächlich dazu, dass eine Handvoll verschiedener ausführbarer Dateien (und ein Symlink) unter /usr/local/bin/ platziert werden :

$ # A snapshot of executables that come bundled with Redis
$ ls -hFG /usr/local/bin/redis-* | sort
/usr/local/bin/redis-benchmark*
/usr/local/bin/redis-check-aof*
/usr/local/bin/redis-check-rdb*
/usr/local/bin/redis-cli*
/usr/local/bin/redis-sentinel@
/usr/local/bin/redis-server*

Während all diese einen bestimmten Verwendungszweck haben, sind die beiden, die Sie wahrscheinlich am meisten interessieren, redis-cli und redis-server , die wir in Kürze skizzieren werden. Aber bevor wir dazu kommen, ist es angebracht, eine Basiskonfiguration einzurichten.



Redis konfigurieren

Redis ist hochgradig konfigurierbar. Obwohl es sofort einsatzbereit läuft, nehmen wir uns eine Minute Zeit, um einige einfache Konfigurationsoptionen festzulegen, die sich auf Datenbankpersistenz und grundlegende Sicherheit beziehen:

$ sudo su root
$ mkdir -p /etc/redis/
$ touch /etc/redis/6379.conf

Schreiben Sie nun Folgendes in /etc/redis/6379.conf . Wir werden im Laufe des Tutorials nach und nach behandeln, was die meisten davon bedeuten:

# /etc/redis/6379.conf

port              6379
daemonize         yes
save              60 1
bind              127.0.0.1
tcp-keepalive     300
dbfilename        dump.rdb
dir               ./
rdbcompression    yes

Die Redis-Konfiguration ist selbstdokumentierend, mit dem Beispiel redis.conf Datei in der Redis-Quelle für Ihr Lesevergnügen. Wenn Sie Redis in einem Produktionssystem verwenden, lohnt es sich, alle Ablenkungen auszublenden und sich die Zeit zu nehmen, diese Beispieldatei vollständig zu lesen, um sich mit den Besonderheiten von Redis vertraut zu machen und Ihr Setup zu optimieren.

Einige Tutorials, einschließlich Teilen der Redis-Dokumentation, schlagen möglicherweise auch vor, das Shell-Skript install_server.sh auszuführen befindet sich in redis/utils/install_server.sh . Sie können dies auf jeden Fall gerne als umfassendere Alternative zu den oben genannten ausführen, aber beachten Sie einige Feinheiten zu install_server.sh :

  • Es funktioniert nicht unter Mac OS X, sondern nur unter Debian und Ubuntu Linux.
  • Es fügt einen umfassenderen Satz von Konfigurationsoptionen in /etc/redis/6379.conf ein .
  • Es wird ein System V init schreiben Skript nach /etc/init.d/redis_6379 damit können Sie sudo service redis_6379 start ausführen .

Die Redis-Schnellstartanleitung enthält auch einen Abschnitt über eine korrektere Redis-Einrichtung, aber die obigen Konfigurationsoptionen sollten für dieses Tutorial und den Einstieg völlig ausreichend sein.

Sicherheitshinweis: Vor einigen Jahren hat der Autor von Redis auf Sicherheitslücken in früheren Versionen von Redis hingewiesen, wenn keine Konfiguration gesetzt wurde. Redis 3.2 (die aktuelle Version 5.0.3 ab März 2019) hat Schritte unternommen, um dieses Eindringen zu verhindern, indem der protected-mode eingestellt wurde Option auf yes standardmäßig.

Wir setzen ausdrücklich bind 127.0.0.1 um Redis nur von der localhost-Schnittstelle auf Verbindungen warten zu lassen, obwohl Sie diese Whitelist in einem echten Produktionsserver erweitern müssten. Der Sinn des protected-mode dient als Sicherheitsmaßnahme, die dieses Bind-to-localhost-Verhalten nachahmt, wenn Sie nichts anderes unter bind angeben Option.

Nachdem dies geklärt ist, können wir uns nun mit der Verwendung von Redis selbst befassen.



Zehn Minuten bis zur Redis

Dieser Abschnitt vermittelt Ihnen gerade genug Wissen über Redis, um gefährlich zu sein, und skizziert das Design und die grundlegende Verwendung.


Erste Schritte

Redis hat eine Client-Server-Architektur und verwendet ein Request-Response-Modell . Das bedeutet, dass Sie (der Client) sich über eine TCP-Verbindung mit einem Redis-Server verbinden, standardmäßig auf Port 6379. Sie fordern eine Aktion an (z. B. eine Form des Lesens, Schreibens, Abrufens, Einstellens oder Aktualisierens) und der Server serviert Sie unterstützen eine Antwort.

Es kann viele Clients geben, die mit demselben Server kommunizieren, und genau darum geht es bei Redis oder jeder Client-Server-Anwendung. Jeder Client führt einen (normalerweise blockierenden) Lesevorgang auf einem Socket durch und wartet auf die Serverantwort.

Das cli in redis-cli steht für Befehlszeilenschnittstelle , und der server im redis-server ist für den Betrieb eines Servers. Genauso wie Sie python ausführen würden In der Befehlszeile können Sie redis-cli ausführen um in eine interaktive REPL (Read Eval Print Loop) zu springen, wo Sie Client-Befehle direkt von der Shell aus ausführen können.

Zuerst müssen Sie jedoch redis-server starten damit Sie einen laufenden Redis-Server haben, mit dem Sie sprechen können. Eine gängige Methode, dies in der Entwicklung zu tun, besteht darin, einen Server auf localhost zu starten (IPv4-Adresse 127.0.0.1 ), was die Standardeinstellung ist, sofern Sie Redis nichts anderes mitteilen. Sie können auch redis-server übergeben der Name Ihrer Konfigurationsdatei, was der Angabe aller ihrer Schlüssel-Wert-Paare als Befehlszeilenargumente ähnelt:

$ redis-server /etc/redis/6379.conf
31829:C 07 Mar 2019 08:45:04.030 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
31829:C 07 Mar 2019 08:45:04.030 # Redis version=5.0.3, bits=64, commit=00000000, modified=0, pid=31829, just started
31829:C 07 Mar 2019 08:45:04.030 # Configuration loaded

Wir setzen daemonize Konfigurationsoption auf yes , sodass der Server im Hintergrund läuft. (Andernfalls verwenden Sie --daemonize yes als Option für redis-server .)

Jetzt können Sie Redis REPL starten. Geben Sie redis-cli ein auf Ihrer Befehlszeile. Sie sehen den host:port des Servers Paar gefolgt von einem > Eingabeaufforderung:

127.0.0.1:6379>

Hier ist einer der einfachsten Redis-Befehle, PING , die nur die Verbindung zum Server testet und "PONG" zurückgibt wenn alles in Ordnung ist:

127.0.0.1:6379> PING
PONG

Bei Redis-Befehlen wird die Groß-/Kleinschreibung nicht beachtet, obwohl dies bei ihren Python-Gegenstücken definitiv nicht der Fall ist.

Hinweis: Als weitere Plausibilitätsprüfung können Sie mit pgrep nach der Prozess-ID des Redis-Servers suchen :

$ pgrep redis-server
26983

Verwenden Sie zum Beenden des Servers pkill redis-server von der Kommandozeile. Unter Mac OS X können Sie auch redis-cli shutdown verwenden .

Als Nächstes verwenden wir einige der gängigen Redis-Befehle und vergleichen sie damit, wie sie in reinem Python aussehen würden.



Redis als Python-Wörterbuch

Redis steht für Remote Dictionary Service .

„Du meinst, wie ein Python-Wörterbuch?“ Sie können fragen.

Ja. Im Großen und Ganzen gibt es viele Parallelen, die Sie zwischen einem Python-Wörterbuch (oder einer generischen Hash-Tabelle) und dem, was Redis ist und tut, ziehen können:

  • Eine Redis-Datenbank enthält Schlüssel:Wert paart und unterstützt Befehle wie GET , SET , und DEL , sowie mehrere hundert zusätzliche Befehle.

  • Redis Schlüssel sind immer Strings.

  • Redis Werte kann eine Anzahl unterschiedlicher Datentypen sein. In diesem Tutorial behandeln wir einige der wichtigeren Wertdatentypen:string , list , hashes , und sets . Einige erweiterte Typen umfassen Geodatenelemente und den neuen Stream-Typ.

  • Viele Redis-Befehle arbeiten in konstanter O(1)-Zeit, genau wie das Abrufen eines Werts aus einem Python-dict oder eine beliebige Hash-Tabelle.

Der Redis-Schöpfer Salvatore Sanfilippo würde den Vergleich einer Redis-Datenbank mit einem Plain-Vanilla-Python-dict wahrscheinlich nicht mögen . Er nennt das Projekt einen „Datenstruktur-Server“ (anstelle eines Schlüssel-Wert-Speichers wie Memcached), da Redis das Speichern zusätzlicher Arten von Schlüssel:Wert unterstützt Datentypen außer string:string . Aber für unsere Zwecke hier ist es ein nützlicher Vergleich, wenn Sie mit dem Dictionary-Objekt von Python vertraut sind.

Lassen Sie uns einsteigen und anhand von Beispielen lernen. Unsere erste Spielzeugdatenbank (mit der ID 0) wird eine Abbildung von Land:Hauptstadt sein , wobei wir SET verwenden um Schlüssel-Wert-Paare festzulegen:

127.0.0.1:6379> SET Bahamas Nassau
OK
127.0.0.1:6379> SET Croatia Zagreb
OK
127.0.0.1:6379> GET Croatia
"Zagreb"
127.0.0.1:6379> GET Japan
(nil)

Die entsprechende Anweisungsfolge würde in reinem Python so aussehen:

>>>
>>> capitals = {}
>>> capitals["Bahamas"] = "Nassau"
>>> capitals["Croatia"] = "Zagreb"
>>> capitals.get("Croatia")
'Zagreb'
>>> capitals.get("Japan")  # None

Wir verwenden capitals.get("Japan") statt capitals["Japan"] da Redis nil zurückgibt statt eines Fehlers, wenn ein Schlüssel nicht gefunden wird, was analog zu Pythons None ist .

Mit Redis können Sie auch mehrere Schlüssel-Wert-Paare in einem Befehl, MSET, festlegen und abrufen und MGET bzw.:

127.0.0.1:6379> MSET Lebanon Beirut Norway Oslo France Paris
OK
127.0.0.1:6379> MGET Lebanon Norway Bahamas
1) "Beirut"
2) "Oslo"
3) "Nassau"

Am nächsten kommt es in Python mit dict.update() :

>>>
>>> capitals.update({
...     "Lebanon": "Beirut",
...     "Norway": "Oslo",
...     "France": "Paris",
... })
>>> [capitals.get(k) for k in ("Lebanon", "Norway", "Bahamas")]
['Beirut', 'Oslo', 'Nassau']

Wir verwenden .get() statt .__getitem__() um das Verhalten von Redis nachzuahmen, einen nullähnlichen Wert zurückzugeben, wenn kein Schlüssel gefunden wird.

Als drittes Beispiel der EXISTS Der Befehl tut das, wonach er sich anhört, nämlich zu prüfen, ob ein Schlüssel vorhanden ist:

127.0.0.1:6379> EXISTS Norway
(integer) 1
127.0.0.1:6379> EXISTS Sweden
(integer) 0

Python hat den in Schlüsselwort, um dasselbe zu testen, das zu dict.__contains__(key) weiterleitet :

>>>
>>> "Norway" in capitals
True
>>> "Sweden" in capitals
False

Diese wenigen Beispiele sollen anhand von nativem Python zeigen, was auf hoher Ebene mit einigen gängigen Redis-Befehlen passiert. Hier gibt es keine Client-Server-Komponente zu den Python-Beispielen und redis-py hat das Bild noch nicht betreten. Dies soll nur beispielhaft die Redis-Funktionalität zeigen.

Hier ist eine Zusammenfassung der wenigen Redis-Befehle, die Sie gesehen haben, und ihrer funktionellen Python-Äquivalente:

capitals["Bahamas"] = "Nassau"

capitals.get("Croatia")

capitals.update(
    {
        "Lebanon": "Beirut",
        "Norway": "Oslo",
        "France": "Paris",
    }
)

[capitals[k] for k in ("Lebanon", "Norway", "Bahamas")]

"Norway" in capitals

Die Python Redis-Clientbibliothek, redis-py , auf die Sie gleich in diesem Artikel eingehen werden, macht die Dinge anders. Es kapselt eine tatsächliche TCP-Verbindung zu einem Redis-Server und sendet Rohbefehle als mit dem REdis Serialization Protocol (RESP) serialisierte Bytes an den Server. Es nimmt dann die Rohantwort und parst sie zurück in ein Python-Objekt wie bytes , int , oder sogar datetime.datetime .

Hinweis :Bisher haben Sie über das interaktive redis-cli mit dem Redis-Server gesprochen ERSATZ Sie können Befehle auch direkt ausführen, so wie Sie den Namen eines Skripts an python übergeben würden ausführbare Datei wie python myscript.py .

Bisher haben Sie einige der grundlegenden Datentypen von Redis gesehen, bei denen es sich um eine Zuordnung von string:string handelt . Während dieses Schlüsselwertpaar in den meisten Schlüsselwertspeichern üblich ist, bietet Redis eine Reihe anderer möglicher Werttypen, die Sie als Nächstes sehen werden.



Mehr Datentypen in Python vs. Redis

Bevor Sie redis-py starten Python-Client ist es auch hilfreich, ein grundlegendes Verständnis für einige weitere Redis-Datentypen zu haben. Um es klar zu sagen, alle Redis-Schlüssel sind Zeichenfolgen. Es ist der Wert, der zusätzlich zu den in den bisherigen Beispielen verwendeten String-Werten Datentypen (oder Strukturen) annehmen kann.

Ein Hash ist eine Abbildung von string:string , genannt Feldwert Paare, die sich unter einem Top-Level-Schlüssel befinden:

127.0.0.1:6379> HSET realpython url "https://realpython.com/"
(integer) 1
127.0.0.1:6379> HSET realpython github realpython
(integer) 1
127.0.0.1:6379> HSET realpython fullname "Real Python"
(integer) 1

Dadurch werden drei Feld-Wert-Paare für einen Schlüssel festgelegt , "realpython" . Wenn Sie an die Terminologie und Objekte von Python gewöhnt sind, kann dies verwirrend sein. Ein Redis-Hash ist ungefähr analog zu einem Python-dict das eine Ebene tief verschachtelt ist:

data = {
    "realpython": {
        "url": "https://realpython.com/",
        "github": "realpython",
        "fullname": "Real Python",
    }
}

Die Felder von Redis ähneln den Python-Schlüsseln jedes verschachtelten Schlüssel-Wert-Paares im inneren Wörterbuch oben. Redis behält sich den Begriff Schlüssel vor für den Datenbankschlüssel der obersten Ebene, der die Hash-Struktur selbst enthält.

Genauso wie es MSET gibt für einfache string:string Schlüssel-Wert-Paare gibt es auch HMSET für Hashes, um mehrere Paare innerhalb festzulegen das Hash-Wert-Objekt:

127.0.0.1:6379> HMSET pypa url "https://www.pypa.io/" github pypa fullname "Python Packaging Authority"
OK
127.0.0.1:6379> HGETALL pypa
1) "url"
2) "https://www.pypa.io/"
3) "github"
4) "pypa"
5) "fullname"
6) "Python Packaging Authority"

Mit HMSET ist wahrscheinlich eine engere Parallele für die Art und Weise, wie wir data zugewiesen haben in ein obiges verschachteltes Wörterbuch, anstatt jedes verschachtelte Paar zu setzen, wie es mit HSET getan wird .

Zwei weitere Werttypen sind Listen und Sätze , der als Redis-Wert einen Hash oder eine Zeichenfolge ersetzen kann. Sie sind weitgehend so, wie sie klingen, also werde ich Ihre Zeit nicht mit zusätzlichen Beispielen in Anspruch nehmen. Hashes, Listen und Mengen haben jeweils einige Befehle, die für diesen bestimmten Datentyp spezifisch sind, die in einigen Fällen durch ihren Anfangsbuchstaben gekennzeichnet sind:

  • Hashes: Befehle zum Arbeiten mit Hashes beginnen mit einem H , wie zum Beispiel HSET , HGET , oder HMSET .

  • Sets: Befehle zum Arbeiten mit Sets beginnen mit einem S , wie zum Beispiel SCARD , die die Anzahl der Elemente bei dem festgelegten Wert erhält, der einem bestimmten Schlüssel entspricht.

  • Listen: Befehle zum Bearbeiten von Listen beginnen mit einem L oder R . Beispiele sind LPOP und RPUSH . Das L oder R bezieht sich darauf, auf welcher Seite der Liste gearbeitet wird. Einigen Listenbefehlen wird auch ein B vorangestellt , was blockieren bedeutet . Eine blockierende Operation lässt nicht zu, dass andere Operationen sie während ihrer Ausführung unterbrechen. Zum Beispiel BLPOP führt einen blockierenden Left-Pop auf einer Listenstruktur aus.

Hinweis: Ein bemerkenswertes Merkmal des Listentyps von Redis ist, dass es sich um eine verknüpfte Liste und nicht um ein Array handelt. Das bedeutet, dass das Anhängen O(1) ist, während das Indizieren bei einer beliebigen Indexnummer O(N) ist.

Hier ist eine kurze Liste von Befehlen, die speziell für die Datentypen string, hash, list und set in Redis gelten:

Typ Befehle
Sätze SADD , SCARD , SDIFF , SDIFFSTORE , SINTER , SINTERSTORE , SISMEMBER , SMEMBERS , SMOVE , SPOP , SRANDMEMBER , SREM , SSCAN , SUNION , SUNIONSTORE
Hashes HDEL , HEXISTS , HGET , HGETALL , HINCRBY , HINCRBYFLOAT , HKEYS , HLEN , HMGET , HMSET , HSCAN , HSET , HSETNX , HSTRLEN , HVALS
Listen BLPOP , BRPOP , BRPOPLPUSH , LINDEX , LINSERT , LLEN , LPOP , LPUSH , LPUSHX , LRANGE , LREM , LSET , LTRIM , RPOP , RPOPLPUSH , RPUSH , RPUSHX
Strings APPEND , BITCOUNT , BITFIELD , BITOP , BITPOS , DECR , DECRBY , GET , GETBIT , GETRANGE , GETSET , INCR , INCRBY , INCRBYFLOAT , MGET , MSET , MSETNX , PSETEX , SET , SETBIT , SETEX , SETNX , SETRANGE , STRLEN

Diese Tabelle ist kein vollständiges Bild der Redis-Befehle und -Typen. Es gibt ein Sammelsurium fortschrittlicherer Datentypen, wie z. B. Geodatenelemente, sortierte Sätze und HyperLogLog. Auf der Seite mit den Redis-Befehlen können Sie nach Datenstrukturgruppe filtern. Es gibt auch eine Zusammenfassung der Datentypen und eine Einführung in die Redis-Datentypen.

Da wir zu Python übergehen werden, können Sie jetzt Ihre Spielzeugdatenbank mit FLUSHDB löschen und beenden Sie das redis-cli ERSATZ:

127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> QUIT

Dies bringt Sie zurück zu Ihrem Shell-Prompt. Sie können redis-server verlassen läuft im Hintergrund, da Sie es auch für den Rest des Tutorials benötigen.




Mit redis-py :Redis in Python

Nachdem Sie nun einige Grundlagen von Redis gemeistert haben, ist es an der Zeit, in redis-py einzusteigen , dem Python-Client, mit dem Sie über eine benutzerfreundliche Python-API mit Redis kommunizieren können.


Erste Schritte

redis-py ist eine bewährte Python-Client-Bibliothek, mit der Sie über Python-Aufrufe direkt mit einem Redis-Server kommunizieren können:

$ python -m pip install redis

Stellen Sie als Nächstes sicher, dass Ihr Redis-Server noch im Hintergrund läuft. Sie können mit pgrep redis-server nachsehen , und wenn Sie leer ausgehen, starten Sie einen lokalen Server mit redis-server /etc/redis/6379.conf neu .

Kommen wir nun zum Python-zentrierten Teil der Dinge. Hier ist die „Hallo Welt“ von redis-py :

>>>
 1>>> import redis
 2>>> r = redis.Redis()
 3>>> r.mset({"Croatia": "Zagreb", "Bahamas": "Nassau"})
 4True
 5>>> r.get("Bahamas")
 6b'Nassau'

Redis , verwendet in Zeile 2, ist die zentrale Klasse des Pakets und das Arbeitspferd, mit dem Sie (fast) jeden Redis-Befehl ausführen. Die TCP-Socket-Verbindung und -Wiederverwendung erfolgt hinter den Kulissen für Sie, und Sie rufen Redis-Befehle mithilfe von Methoden für die Klasseninstanz r auf .

Beachten Sie auch, dass der Typ des zurückgegebenen Objekts b'Nassau' in Zeile 6 sind Pythons bytes Typ, nicht str . Es sind bytes statt str das ist der häufigste Rückgabetyp bei redis-py , daher müssen Sie möglicherweise r.get("Bahamas").decode("utf-8") aufrufen abhängig davon, was Sie tatsächlich mit dem zurückgegebenen Bytestring machen möchten.

Kommt Ihnen der obige Code bekannt vor? Die Methoden stimmen in fast allen Fällen mit dem Namen des Redis-Befehls überein, der dasselbe tut. Hier haben Sie r.mset() aufgerufen und r.get() , die MSET entsprechen und GET in der nativen Redis-API.

Das bedeutet auch, dass HGETALL wird zu r.hgetall() , PING wird zu r.ping() , und so weiter. Es gibt ein paar Ausnahmen, aber die Regel gilt für die große Mehrheit der Befehle.

Während die Redis-Befehlsargumente normalerweise in eine ähnlich aussehende Methodensignatur übersetzt werden, nehmen sie Python-Objekte entgegen. Beispielsweise der Aufruf von r.mset() verwendet im obigen Beispiel ein Python-dict als erstes Argument anstelle einer Folge von Bytestrings.

Wir haben die Redis gebaut Instanz r ohne Argumente, aber mit einer Reihe von Parametern, falls Sie sie brauchen:

# From redis/client.py
class Redis(object):
    def __init__(self, host='localhost', port=6379,
                 db=0, password=None, socket_timeout=None,
                 # ...

Sie können sehen, dass der Standard hostname:port Paar ist localhost:6379 , und genau das brauchen wir im Fall unseres lokal gehaltenen redis-server Beispiel.

Die db Parameter ist die Datenbanknummer. Sie können mehrere Datenbanken gleichzeitig in Redis verwalten, und jede wird durch eine Ganzzahl identifiziert. Die maximale Anzahl von Datenbanken beträgt standardmäßig 16.

Wenn Sie nur redis-cli ausführen Von der Befehlszeile aus startet dies bei Datenbank 0. Verwenden Sie den -n Flag, um eine neue Datenbank zu starten, wie in redis-cli -n 5 .



Zulässige Schlüsseltypen

Eine wissenswerte Sache ist das redis-py erfordert, dass Sie ihm Schlüssel übergeben, die bytes sind , str , int , oder float . (Die letzten 3 dieser Typen werden in bytes konvertiert bevor sie an den Server gesendet werden.)

Stellen Sie sich einen Fall vor, in dem Sie Kalenderdaten als Schlüssel verwenden möchten:

>>>
>>> import datetime
>>> today = datetime.date.today()
>>> visitors = {"dan", "jon", "alex"}
>>> r.sadd(today, *visitors)
Traceback (most recent call last):
# ...
redis.exceptions.DataError: Invalid input of type: 'date'.
Convert to a byte, string or number first.

Sie müssen das Python-date explizit konvertieren Objekt zu str , was Sie mit .isoformat() tun können :

>>>
>>> stoday = today.isoformat()  # Python 3.7+, or use str(today)
>>> stoday
'2019-03-10'
>>> r.sadd(stoday, *visitors)  # sadd: set-add
3
>>> r.smembers(stoday)
{b'dan', b'alex', b'jon'}
>>> r.scard(today.isoformat())
3

Um es noch einmal zusammenzufassen:Redis selbst erlaubt nur Zeichenfolgen als Schlüssel. redis-py ist etwas liberaler in Bezug auf die akzeptierten Python-Typen, obwohl es letztendlich alles in Bytes konvertiert, bevor es an einen Redis-Server gesendet wird.



Beispiel:PyHats.com

Es ist an der Zeit, ein umfassenderes Beispiel zu nennen. Stellen wir uns vor, wir hätten beschlossen, eine lukrative Website, PyHats.com, zu starten, die unverschämt überteuerte Hüte an jeden verkauft, der sie kaufen möchte, und Sie mit dem Aufbau der Website beauftragt.

Sie verwenden Redis, um einen Teil des Produktkatalogs, der Inventarisierung und der Bot-Traffic-Erkennung für PyHats.com zu handhaben.

Es ist der erste Tag für die Website und wir werden drei Hüte in limitierter Auflage verkaufen. Jeder Hut wird in einem Redis-Hash aus Feldwertpaaren gespeichert, und der Hash hat einen Schlüssel, der eine zufällige ganze Zahl vorangestellt ist, z. B. hat:56854717 . Mit dem hat: Präfix ist eine Redis-Konvention zum Erstellen einer Art Namespace innerhalb einer Redis-Datenbank:

import random

random.seed(444)
hats = {f"hat:{random.getrandbits(32)}": i for i in (
    {
        "color": "black",
        "price": 49.99,
        "style": "fitted",
        "quantity": 1000,
        "npurchased": 0,
    },
    {
        "color": "maroon",
        "price": 59.99,
        "style": "hipster",
        "quantity": 500,
        "npurchased": 0,
    },
    {
        "color": "green",
        "price": 99.99,
        "style": "baseball",
        "quantity": 200,
        "npurchased": 0,
    })
}

Beginnen wir mit der Datenbank 1 da wir die Datenbank 0 verwendet haben in einem vorherigen Beispiel:

>>>
>>> r = redis.Redis(db=1)

Um diese Daten anfänglich in Redis zu schreiben, können wir .hmset() verwenden (Hash Multi-Set) und ruft es für jedes Wörterbuch auf. Das „multi“ ist ein Verweis auf das Festlegen mehrerer Feld-Wert-Paare, wobei „field“ in diesem Fall einem Schlüssel eines der verschachtelten Wörterbücher in hats entspricht :

 1>>> with r.pipeline() as pipe:
 2...    for h_id, hat in hats.items():
 3...        pipe.hmset(h_id, hat)
 4...    pipe.execute()
 5Pipeline<ConnectionPool<Connection<host=localhost,port=6379,db=1>>>
 6Pipeline<ConnectionPool<Connection<host=localhost,port=6379,db=1>>>
 7Pipeline<ConnectionPool<Connection<host=localhost,port=6379,db=1>>>
 8[True, True, True]
 9
10>>> r.bgsave()
11True

Der obige Codeblock stellt auch das Konzept von Redis Pipelining vor , wodurch Sie die Anzahl der Roundtrip-Transaktionen reduzieren können, die Sie zum Schreiben oder Lesen von Daten auf Ihrem Redis-Server benötigen. Wenn Sie einfach r.hmset() aufgerufen hätten dreimal, dann würde dies einen Hin- und Her-Umlaufvorgang für jede geschriebene Zeile erfordern.

Bei einer Pipeline werden alle Befehle clientseitig zwischengespeichert und dann auf einmal per pipe.hmset() auf einen Schlag versendet in Zeile 3. Deshalb die drei True Antworten werden alle auf einmal zurückgegeben, wenn Sie pipe.execute() aufrufen in Zeile 4. Sie werden in Kürze einen fortgeschritteneren Anwendungsfall für eine Pipeline sehen.

Hinweis :Die Redis-Dokumentation bietet ein Beispiel dafür, wie man dasselbe mit redis-cli macht , wo Sie den Inhalt einer lokalen Datei weiterleiten können, um Masseneinfügungen durchzuführen.

Lassen Sie uns kurz überprüfen, ob alles in unserer Redis-Datenbank vorhanden ist:

>>>
>>> pprint(r.hgetall("hat:56854717"))
{b'color': b'green',
 b'npurchased': b'0',
 b'price': b'99.99',
 b'quantity': b'200',
 b'style': b'baseball'}

>>> r.keys()  # Careful on a big DB. keys() is O(N)
[b'56854717', b'1236154736', b'1326692461']

Als Erstes möchten wir simulieren, was passiert, wenn ein Nutzer auf Kaufen klickt . Wenn der Artikel auf Lager ist, erhöhen Sie seinen npurchased um 1 und verringern Sie seine quantity (Inventar) um 1. Sie können .hincrby() verwenden dazu:

>>>
>>> r.hincrby("hat:56854717", "quantity", -1)
199
>>> r.hget("hat:56854717", "quantity")
b'199'
>>> r.hincrby("hat:56854717", "npurchased", 1)
1

Hinweis :HINCRBY arbeitet immer noch mit einem Hash-Wert, der eine Zeichenfolge ist, aber es versucht, die Zeichenfolge als 64-Bit-Ganzzahl mit Vorzeichen auf Basis 10 zu interpretieren, um die Operation auszuführen.

Dies gilt für andere Befehle, die sich auf das Inkrementieren und Dekrementieren anderer Datenstrukturen beziehen, nämlich INCR , INCRBY , INCRBYFLOAT , ZINCRBY , und HINCRBYFLOAT . Sie erhalten eine Fehlermeldung, wenn die Zeichenfolge am Wert nicht als Ganzzahl dargestellt werden kann.

Es ist jedoch nicht wirklich so einfach. Ändern der quantity und npurchased in zwei Codezeilen verbirgt sich die Realität, dass ein Klick, ein Kauf und eine Zahlung mehr beinhalten als dies. We need to do a few more checks to make sure we don’t leave someone with a lighter wallet and no hat:

  • Step 1: Check if the item is in stock, or otherwise raise an exception on the backend.
  • Step 2: If it is in stock, then execute the transaction, decrease the quantity field, and increase the npurchased field.
  • Step 3: Be alert for any changes that alter the inventory in between the first two steps (a race condition).

Step 1 is relatively straightforward:it consists of an .hget() to check the available quantity.

Step 2 is a little bit more involved. The pair of increase and decrease operations need to be executed atomically :either both should be completed successfully, or neither should be (in the case that at least one fails).

With client-server frameworks, it’s always crucial to pay attention to atomicity and look out for what could go wrong in instances where multiple clients are trying to talk to the server at once. The answer to this in Redis is to use a transaction block, meaning that either both or neither of the commands get through.

In redis-py , Pipeline is a transactional pipeline class by default. This means that, even though the class is actually named for something else (pipelining), it can be used to create a transaction block also.

In Redis, a transaction starts with MULTI and ends with EXEC :

 1127.0.0.1:6379> MULTI
 2127.0.0.1:6379> HINCRBY 56854717 quantity -1
 3127.0.0.1:6379> HINCRBY 56854717 npurchased 1
 4127.0.0.1:6379> EXEC

MULTI (Line 1) marks the start of the transaction, and EXEC (Line 4) marks the end. Everything in between is executed as one all-or-nothing buffered sequence of commands. This means that it will be impossible to decrement quantity (Line 2) but then have the balancing npurchased increment operation fail (Line 3).

Let’s circle back to Step 3:we need to be aware of any changes that alter the inventory in between the first two steps.

Step 3 is the trickiest. Let’s say that there is one lone hat remaining in our inventory. In between the time that User A checks the quantity of hats remaining and actually processes their transaction, User B also checks the inventory and finds likewise that there is one hat listed in stock. Both users will be allowed to purchase the hat, but we have 1 hat to sell, not 2, so we’re on the hook and one user is out of their money. Not good.

Redis has a clever answer for the dilemma in Step 3:it’s called optimistic locking , and is different than how typical locking works in an RDBMS such as PostgreSQL. Optimistic locking, in a nutshell, means that the calling function (client) does not acquire a lock, but rather monitors for changes in the data it is writing to during the time it would have held a lock . If there’s a conflict during that time, the calling function simply tries the whole process again.

You can effect optimistic locking by using the WATCH command (.watch() in redis-py ), which provides a check-and-set behavior.

Let’s introduce a big chunk of code and walk through it afterwards step by step. You can picture buyitem() as being called any time a user clicks on a Buy Now or Purchase button. Its purpose is to confirm the item is in stock and take an action based on that result, all in a safe manner that looks out for race conditions and retries if one is detected:

 1import logging
 2import redis
 3
 4logging.basicConfig()
 5
 6class OutOfStockError(Exception):
 7    """Raised when PyHats.com is all out of today's hottest hat"""
 8
 9def buyitem(r: redis.Redis, itemid: int) -> None:
10    with r.pipeline() as pipe:
11        error_count = 0
12        while True:
13            try:
14                # Get available inventory, watching for changes
15                # related to this itemid before the transaction
16                pipe.watch(itemid)
17                nleft: bytes = r.hget(itemid, "quantity")
18                if nleft > b"0":
19                    pipe.multi()
20                    pipe.hincrby(itemid, "quantity", -1)
21                    pipe.hincrby(itemid, "npurchased", 1)
22                    pipe.execute()
23                    break
24                else:
25                    # Stop watching the itemid and raise to break out
26                    pipe.unwatch()
27                    raise OutOfStockError(
28                        f"Sorry, {itemid} is out of stock!"
29                    )
30            except redis.WatchError:
31                # Log total num. of errors by this user to buy this item,
32                # then try the same process again of WATCH/HGET/MULTI/EXEC
33                error_count += 1
34                logging.warning(
35                    "WatchError #%d: %s; retrying",
36                    error_count, itemid
37                )
38    return None

The critical line occurs at Line 16 with pipe.watch(itemid) , which tells Redis to monitor the given itemid for any changes to its value. The program checks the inventory through the call to r.hget(itemid, "quantity") , in Line 17:

16pipe.watch(itemid)
17nleft: bytes = r.hget(itemid, "quantity")
18if nleft > b"0":
19    # Item in stock. Proceed with transaction.

If the inventory gets touched during this short window between when the user checks the item stock and tries to purchase it, then Redis will return an error, and redis-py will raise a WatchError (Line 30). That is, if any of the hash pointed to by itemid changes after the .hget() call but before the subsequent .hincrby() calls in Lines 20 and 21, then we’ll re-run the whole process in another iteration of the while True loop as a result.

This is the “optimistic” part of the locking:rather than letting the client have a time-consuming total lock on the database through the getting and setting operations, we leave it up to Redis to notify the client and user only in the case that calls for a retry of the inventory check.

One key here is in understanding the difference between client-side and server-side operations:

nleft = r.hget(itemid, "quantity")

This Python assignment brings the result of r.hget() client-side. Conversely, methods that you call on pipe effectively buffer all of the commands into one, and then send them to the server in a single request:

16pipe.multi()
17pipe.hincrby(itemid, "quantity", -1)
18pipe.hincrby(itemid, "npurchased", 1)
19pipe.execute()

No data comes back to the client side in the middle of the transactional pipeline. You need to call .execute() (Line 19) to get the sequence of results back all at once.

Even though this block contains two commands, it consists of exactly one round-trip operation from client to server and back.

This means that the client can’t immediately use the result of pipe.hincrby(itemid, "quantity", -1) , from Line 20, because methods on a Pipeline return just the pipe instance itself. We haven’t asked anything from the server at this point. While normally .hincrby() returns the resulting value, you can’t immediately reference it on the client side until the entire transaction is completed.

There’s a catch-22:this is also why you can’t put the call to .hget() into the transaction block. If you did this, then you’d be unable to know if you want to increment the npurchased field yet, since you can’t get real-time results from commands that are inserted into a transactional pipeline.

Finally, if the inventory sits at zero, then we UNWATCH the item ID and raise an OutOfStockError (Line 27), ultimately displaying that coveted Sold Out page that will make our hat buyers desperately want to buy even more of our hats at ever more outlandish prices:

24else:
25    # Stop watching the itemid and raise to break out
26    pipe.unwatch()
27    raise OutOfStockError(
28        f"Sorry, {itemid} is out of stock!"
29    )

Here’s an illustration. Keep in mind that our starting quantity is 199 for hat 56854717 since we called .hincrby() above. Let’s mimic 3 purchases, which should modify the quantity and npurchased fields:

>>>
>>> buyitem(r, "hat:56854717")
>>> buyitem(r, "hat:56854717")
>>> buyitem(r, "hat:56854717")
>>> r.hmget("hat:56854717", "quantity", "npurchased")  # Hash multi-get
[b'196', b'4']

Now, we can fast-forward through more purchases, mimicking a stream of purchases until the stock depletes to zero. Again, picture these coming from a whole bunch of different clients rather than just one Redis instance:

>>>
>>> # Buy remaining 196 hats for item 56854717 and deplete stock to 0
>>> for _ in range(196):
...     buyitem(r, "hat:56854717")
>>> r.hmget("hat:56854717", "quantity", "npurchased")
[b'0', b'200']

Now, when some poor user is late to the game, they should be met with an OutOfStockError that tells our application to render an error message page on the frontend:

>>>
>>> buyitem(r, "hat:56854717")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 20, in buyitem
__main__.OutOfStockError: Sorry, hat:56854717 is out of stock!

Looks like it’s time to restock.



Using Key Expiry

Let’s introduce key expiry , which is another distinguishing feature in Redis. When you expire a key, that key and its corresponding value will be automatically deleted from the database after a certain number of seconds or at a certain timestamp.

In redis-py , one way that you can accomplish this is through .setex() , which lets you set a basic string:string key-value pair with an expiration:

>>>
 1>>> from datetime import timedelta
 2
 3>>> # setex: "SET" with expiration
 4>>> r.setex(
 5...     "runner",
 6...     timedelta(minutes=1),
 7...     value="now you see me, now you don't"
 8... )
 9True

You can specify the second argument as a number in seconds or a timedelta object, as in Line 6 above. I like the latter because it seems less ambiguous and more deliberate.

There are also methods (and corresponding Redis commands, of course) to get the remaining lifetime (time-to-live ) of a key that you’ve set to expire:

>>>
>>> r.ttl("runner")  # "Time To Live", in seconds
58
>>> r.pttl("runner")  # Like ttl, but milliseconds
54368

Below, you can accelerate the window until expiration, and then watch the key expire, after which r.get() will return None and .exists() will return 0 :

>>>
>>> r.get("runner")  # Not expired yet
b"now you see me, now you don't"

>>> r.expire("runner", timedelta(seconds=3))  # Set new expire window
True
>>> # Pause for a few seconds
>>> r.get("runner")
>>> r.exists("runner")  # Key & value are both gone (expired)
0

The table below summarizes commands related to key-value expiration, including the ones covered above. The explanations are taken directly from redis-py method docstrings:

Signature Purpose
r.setex(name, time, value) Sets the value of key name to value that expires in time seconds, where time can be represented by an int or a Python timedelta object
r.psetex(name, time_ms, value) Sets the value of key name to value that expires in time_ms milliseconds, where time_ms can be represented by an int or a Python timedelta object
r.expire(name, time) Sets an expire flag on key name for time seconds, where time can be represented by an int or a Python timedelta object
r.expireat(name, when) Sets an expire flag on key name , where when can be represented as an int indicating Unix time or a Python datetime object
r.persist(name) Removes an expiration on name
r.pexpire(name, time) Sets an expire flag on key name for time milliseconds, and time can be represented by an int or a Python timedelta object
r.pexpireat(name, when) Sets an expire flag on key name , where when can be represented as an int representing Unix time in milliseconds (Unix time * 1000) or a Python datetime object
r.pttl(name) Returns the number of milliseconds until the key name will expire
r.ttl(name) Returns the number of seconds until the key name will expire


PyHats.com, Part 2

A few days after its debut, PyHats.com has attracted so much hype that some enterprising users are creating bots to buy hundreds of items within seconds, which you’ve decided isn’t good for the long-term health of your hat business.

Now that you’ve seen how to expire keys, let’s put it to use on the backend of PyHats.com.

We’re going to create a new Redis client that acts as a consumer (or watcher) and processes a stream of incoming IP addresses, which in turn may come from multiple HTTPS connections to the website’s server.

The watcher’s goal is to monitor a stream of IP addresses from multiple sources, keeping an eye out for a flood of requests from a single address within a suspiciously short amount of time.

Some middleware on the website server pushes all incoming IP addresses into a Redis list with .lpush() . Here’s a crude way of mimicking some incoming IPs, using a fresh Redis database:

>>>
>>> r = redis.Redis(db=5)
>>> r.lpush("ips", "51.218.112.236")
1
>>> r.lpush("ips", "90.213.45.98")
2
>>> r.lpush("ips", "115.215.230.176")
3
>>> r.lpush("ips", "51.218.112.236")
4

As you can see, .lpush() returns the length of the list after the push operation succeeds. Each call of .lpush() puts the IP at the beginning of the Redis list that is keyed by the string "ips" .

In this simplified simulation, the requests are all technically from the same client, but you can think of them as potentially coming from many different clients and all being pushed to the same database on the same Redis server.

Now, open up a new shell tab or window and launch a new Python REPL. In this shell, you’ll create a new client that serves a very different purpose than the rest, which sits in an endless while True loop and does a blocking left-pop BLPOP call on the ips list, processing each address:

 1# New shell window or tab
 2
 3import datetime
 4import ipaddress
 5
 6import redis
 7
 8# Where we put all the bad egg IP addresses
 9blacklist = set()
10MAXVISITS = 15
11
12ipwatcher = redis.Redis(db=5)
13
14while True:
15    _, addr = ipwatcher.blpop("ips")
16    addr = ipaddress.ip_address(addr.decode("utf-8"))
17    now = datetime.datetime.utcnow()
18    addrts = f"{addr}:{now.minute}"
19    n = ipwatcher.incrby(addrts, 1)
20    if n >= MAXVISITS:
21        print(f"Hat bot detected!:  {addr}")
22        blacklist.add(addr)
23    else:
24        print(f"{now}:  saw {addr}")
25    _ = ipwatcher.expire(addrts, 60)

Let’s walk through a few important concepts.

The ipwatcher acts like a consumer, sitting around and waiting for new IPs to be pushed on the "ips" Redis list. It receives them as bytes , such as b”51.218.112.236”, and makes them into a more proper address object with the ipaddress module:

15_, addr = ipwatcher.blpop("ips")
16addr = ipaddress.ip_address(addr.decode("utf-8"))

Then you form a Redis string key using the address and minute of the hour at which the ipwatcher saw the address, incrementing the corresponding count by 1 and getting the new count in the process:

17now = datetime.datetime.utcnow()
18addrts = f"{addr}:{now.minute}"
19n = ipwatcher.incrby(addrts, 1)

If the address has been seen more than MAXVISITS , then it looks as if we have a PyHats.com web scraper on our hands trying to create the next tulip bubble. Alas, we have no choice but to give this user back something like a dreaded 403 status code.

We use ipwatcher.expire(addrts, 60) to expire the (address minute) combination 60 seconds from when it was last seen. This is to prevent our database from becoming clogged up with stale one-time page viewers.

If you execute this code block in a new shell, you should immediately see this output:

2019-03-11 15:10:41.489214:  saw 51.218.112.236
2019-03-11 15:10:41.490298:  saw 115.215.230.176
2019-03-11 15:10:41.490839:  saw 90.213.45.98
2019-03-11 15:10:41.491387:  saw 51.218.112.236

The output appears right away because those four IPs were sitting in the queue-like list keyed by "ips" , waiting to be pulled out by our ipwatcher . Using .blpop() (or the BLPOP command) will block until an item is available in the list, then pops it off. It behaves like Python’s Queue.get() , which also blocks until an item is available.

Besides just spitting out IP addresses, our ipwatcher has a second job. For a given minute of an hour (minute 1 through minute 60), ipwatcher will classify an IP address as a hat-bot if it sends 15 or more GET requests in that minute.

Switch back to your first shell and mimic a page scraper that blasts the site with 20 requests in a few milliseconds:

for _ in range(20):
    r.lpush("ips", "104.174.118.18")

Finally, toggle back to the second shell holding ipwatcher , and you should see an output like this:

2019-03-11 15:15:43.041363:  saw 104.174.118.18
2019-03-11 15:15:43.042027:  saw 104.174.118.18
2019-03-11 15:15:43.042598:  saw 104.174.118.18
2019-03-11 15:15:43.043143:  saw 104.174.118.18
2019-03-11 15:15:43.043725:  saw 104.174.118.18
2019-03-11 15:15:43.044244:  saw 104.174.118.18
2019-03-11 15:15:43.044760:  saw 104.174.118.18
2019-03-11 15:15:43.045288:  saw 104.174.118.18
2019-03-11 15:15:43.045806:  saw 104.174.118.18
2019-03-11 15:15:43.046318:  saw 104.174.118.18
2019-03-11 15:15:43.046829:  saw 104.174.118.18
2019-03-11 15:15:43.047392:  saw 104.174.118.18
2019-03-11 15:15:43.047966:  saw 104.174.118.18
2019-03-11 15:15:43.048479:  saw 104.174.118.18
Hat bot detected!:  104.174.118.18
Hat bot detected!:  104.174.118.18
Hat bot detected!:  104.174.118.18
Hat bot detected!:  104.174.118.18
Hat bot detected!:  104.174.118.18
Hat bot detected!:  104.174.118.18

Now, Ctrl +C out of the while True loop and you’ll see that the offending IP has been added to your blacklist:

>>>
>>> blacklist
{IPv4Address('104.174.118.18')}

Can you find the defect in this detection system? The filter checks the minute as .minute rather than the last 60 seconds (a rolling minute). Implementing a rolling check to monitor how many times a user has been seen in the last 60 seconds would be trickier. There’s a crafty solution using using Redis’ sorted sets at ClassDojo. Josiah Carlson’s Redis in Action also presents a more elaborate and general-purpose example of this section using an IP-to-location cache table.



Persistence and Snapshotting

One of the reasons that Redis is so fast in both read and write operations is that the database is held in memory (RAM) on the server. However, a Redis database can also be stored (persisted) to disk in a process called snapshotting. The point behind this is to keep a physical backup in binary format so that data can be reconstructed and put back into memory when needed, such as at server startup.

You already enabled snapshotting without knowing it when you set up basic configuration at the beginning of this tutorial with the save option:

# /etc/redis/6379.conf

port              6379
daemonize         yes
save              60 1
bind              127.0.0.1
tcp-keepalive     300
dbfilename        dump.rdb
dir               ./
rdbcompression    yes

The format is save <seconds> <changes> . This tells Redis to save the database to disk if both the given number of seconds and number of write operations against the database occurred. In this case, we’re telling Redis to save the database to disk every 60 seconds if at least one modifying write operation occurred in that 60-second timespan. This is a fairly aggressive setting versus the sample Redis config file, which uses these three save directives:

# Default redis/redis.conf
save 900 1
save 300 10
save 60 10000

An RDB snapshot is a full (rather than incremental) point-in-time capture of the database. (RDB refers to a Redis Database File.) We also specified the directory and file name of the resulting data file that gets written:

# /etc/redis/6379.conf

port              6379
daemonize         yes
save              60 1
bind              127.0.0.1
tcp-keepalive     300
dbfilename        dump.rdb
dir               ./
rdbcompression    yes

This instructs Redis to save to a binary data file called dump.rdb in the current working directory of wherever redis-server was executed from:

$ file -b dump.rdb
data

You can also manually invoke a save with the Redis command BGSAVE :

127.0.0.1:6379> BGSAVE
Background saving started

The “BG” in BGSAVE indicates that the save occurs in the background. This option is available in a redis-py method as well:

>>>
>>> r.lastsave()  # Redis command: LASTSAVE
datetime.datetime(2019, 3, 10, 21, 56, 50)
>>> r.bgsave()
True
>>> r.lastsave()
datetime.datetime(2019, 3, 10, 22, 4, 2)

This example introduces another new command and method, .lastsave() . In Redis, it returns the Unix timestamp of the last DB save, which Python gives back to you as a datetime Objekt. Above, you can see that the r.lastsave() result changes as a result of r.bgsave() .

r.lastsave() will also change if you enable automatic snapshotting with the save configuration option.

To rephrase all of this, there are two ways to enable snapshotting:

  1. Explicitly, through the Redis command BGSAVE or redis-py method .bgsave()
  2. Implicitly, through the save configuration option (which you can also set with .config_set() in redis-py )

RDB snapshotting is fast because the parent process uses the fork() system call to pass off the time-intensive write to disk to a child process, so that the parent process can continue on its way. This is what the background in BGSAVE refers to.

There’s also SAVE (.save() in redis-py ), but this does a synchronous (blocking) save rather than using fork() , so you shouldn’t use it without a specific reason.

Even though .bgsave() occurs in the background, it’s not without its costs. The time for fork() itself to occur can actually be substantial if the Redis database is large enough in the first place.

If this is a concern, or if you can’t afford to miss even a tiny slice of data lost due to the periodic nature of RDB snapshotting, then you should look into the append-only file (AOF) strategy that is an alternative to snapshotting. AOF copies Redis commands to disk in real time, allowing you to do a literal command-based reconstruction by replaying these commands.



Serialization Workarounds

Let’s get back to talking about Redis data structures. With its hash data structure, Redis in effect supports nesting one level deep:

127.0.0.1:6379> hset mykey field1 value1

The Python client equivalent would look like this:

r.hset("mykey", "field1", "value1")

Here, you can think of "field1": "value1" as being the key-value pair of a Python dict, {"field1": "value1"} , while mykey is the top-level key:

Redis Command Pure-Python Equivalent
r.set("key", "value") r = {"key": "value"}
r.hset("key", "field", "value") r = {"key": {"field": "value"}}

But what if you want the value of this dictionary (the Redis hash) to contain something other than a string, such as a list or nested dictionary with strings as values?

Here’s an example using some JSON-like data to make the distinction clearer:

restaurant_484272 = {
    "name": "Ravagh",
    "type": "Persian",
    "address": {
        "street": {
            "line1": "11 E 30th St",
            "line2": "APT 1",
        },
        "city": "New York",
        "state": "NY",
        "zip": 10016,
    }
}

Say that we want to set a Redis hash with the key 484272 and field-value pairs corresponding to the key-value pairs from restaurant_484272 . Redis does not support this directly, because restaurant_484272 is nested:

>>>
>>> r.hmset(484272, restaurant_484272)
Traceback (most recent call last):
# ...
redis.exceptions.DataError: Invalid input of type: 'dict'.
Convert to a byte, string or number first.

You can in fact make this work with Redis. There are two different ways to mimic nested data in redis-py and Redis:

  1. Serialize the values into a string with something like json.dumps()
  2. Use a delimiter in the key strings to mimic nesting in the values

Let’s take a look at an example of each.

Option 1:Serialize the Values Into a String

You can use json.dumps() to serialize the dict into a JSON-formatted string:

>>>
>>> import json
>>> r.set(484272, json.dumps(restaurant_484272))
True

If you call .get() , the value you get back will be a bytes object, so don’t forget to deserialize it to get back the original object. json.dumps() and json.loads() are inverses of each other, for serializing and deserializing data, respectively:

>>>
>>> from pprint import pprint
>>> pprint(json.loads(r.get(484272)))
{'address': {'city': 'New York',
             'state': 'NY',
             'street': '11 E 30th St',
             'zip': 10016},
 'name': 'Ravagh',
 'type': 'Persian'}

This applies to any serialization protocol, with another common choice being yaml :

>>>
>>> import yaml  # python -m pip install PyYAML
>>> yaml.dump(restaurant_484272)
'address: {city: New York, state: NY, street: 11 E 30th St, zip: 10016}\nname: Ravagh\ntype: Persian\n'

No matter what serialization protocol you choose to go with, the concept is the same:you’re taking an object that is unique to Python and converting it to a bytestring that is recognized and exchangeable across multiple languages.

Option 2:Use a Delimiter in Key Strings

There’s a second option that involves mimicking “nestedness” by concatenating multiple levels of keys in a Python dict . This consists of flattening the nested dictionary through recursion, so that each key is a concatenated string of keys, and the values are the deepest-nested values from the original dictionary. Consider our dictionary object restaurant_484272 :

restaurant_484272 = {
    "name": "Ravagh",
    "type": "Persian",
    "address": {
        "street": {
            "line1": "11 E 30th St",
            "line2": "APT 1",
        },
        "city": "New York",
        "state": "NY",
        "zip": 10016,
    }
}

We want to get it into this form:

{
    "484272:name":                     "Ravagh",
    "484272:type":                     "Persian",
    "484272:address:street:line1":     "11 E 30th St",
    "484272:address:street:line2":     "APT 1",
    "484272:address:city":             "New York",
    "484272:address:state":            "NY",
    "484272:address:zip":              "10016",
}

That’s what setflat_skeys() below does, with the added feature that it does inplace .set() operations on the Redis instance itself rather than returning a copy of the input dictionary:

 1from collections.abc import MutableMapping
 2
 3def setflat_skeys(
 4    r: redis.Redis,
 5    obj: dict,
 6    prefix: str,
 7    delim: str = ":",
 8    *,
 9    _autopfix=""
10) -> None:
11    """Flatten `obj` and set resulting field-value pairs into `r`.
12
13    Calls `.set()` to write to Redis instance inplace and returns None.
14
15    `prefix` is an optional str that prefixes all keys.
16    `delim` is the delimiter that separates the joined, flattened keys.
17    `_autopfix` is used in recursive calls to created de-nested keys.
18
19    The deepest-nested keys must be str, bytes, float, or int.
20    Otherwise a TypeError is raised.
21    """
22    allowed_vtypes = (str, bytes, float, int)
23    for key, value in obj.items():
24        key = _autopfix + key
25        if isinstance(value, allowed_vtypes):
26            r.set(f"{prefix}{delim}{key}", value)
27        elif isinstance(value, MutableMapping):
28            setflat_skeys(
29                r, value, prefix, delim, _autopfix=f"{key}{delim}"
30            )
31        else:
32            raise TypeError(f"Unsupported value type: {type(value)}")

The function iterates over the key-value pairs of obj , first checking the type of the value (Line 25) to see if it looks like it should stop recursing further and set that key-value pair. Otherwise, if the value looks like a dict (Line 27), then it recurses into that mapping, adding the previously seen keys as a key prefix (Line 28).

Let’s see it at work:

>>>
>>> r.flushdb()  # Flush database: clear old entries
>>> setflat_skeys(r, restaurant_484272, 484272)

>>> for key in sorted(r.keys("484272*")):  # Filter to this pattern
...     print(f"{repr(key):35}{repr(r.get(key)):15}")
...
b'484272:address:city'             b'New York'
b'484272:address:state'            b'NY'
b'484272:address:street:line1'     b'11 E 30th St'
b'484272:address:street:line2'     b'APT 1'
b'484272:address:zip'              b'10016'
b'484272:name'                     b'Ravagh'
b'484272:type'                     b'Persian'

>>> r.get("484272:address:street:line1")
b'11 E 30th St'

The final loop above uses r.keys("484272*") , where "484272*" is interpreted as a pattern and matches all keys in the database that begin with "484272" .

Notice also how setflat_skeys() calls just .set() rather than .hset() , because we’re working with plain string:string field-value pairs, and the 484272 ID key is prepended to each field string.



Encryption

Another trick to help you sleep well at night is to add symmetric encryption before sending anything to a Redis server. Consider this as an add-on to the security that you should make sure is in place by setting proper values in your Redis configuration. The example below uses the cryptography package:

$ python -m pip install cryptography

To illustrate, pretend that you have some sensitive cardholder data (CD) that you never want to have sitting around in plaintext on any server, no matter what. Before caching it in Redis, you can serialize the data and then encrypt the serialized string using Fernet:

>>>
>>> import json
>>> from cryptography.fernet import Fernet

>>> cipher = Fernet(Fernet.generate_key())
>>> info = {
...     "cardnum": 2211849528391929,
...     "exp": [2020, 9],
...     "cv2": 842,
... }

>>> r.set(
...     "user:1000",
...     cipher.encrypt(json.dumps(info).encode("utf-8"))
... )

>>> r.get("user:1000")
b'gAAAAABcg8-LfQw9TeFZ1eXbi'  # ... [truncated]

>>> cipher.decrypt(r.get("user:1000"))
b'{"cardnum": 2211849528391929, "exp": [2020, 9], "cv2": 842}'

>>> json.loads(cipher.decrypt(r.get("user:1000")))
{'cardnum': 2211849528391929, 'exp': [2020, 9], 'cv2': 842}

Because info contains a value that is a list , you’ll need to serialize this into a string that’s acceptable by Redis. (You could use json , yaml , or any other serialization for this.) Next, you encrypt and decrypt that string using the cipher Objekt. You need to deserialize the decrypted bytes using json.loads() so that you can get the result back into the type of your initial input, a dict .

Note :Fernet uses AES 128 encryption in CBC mode. See the cryptography docs for an example of using AES 256. Whatever you choose to do, use cryptography , not pycrypto (imported as Crypto ), which is no longer actively maintained.

If security is paramount, encrypting strings before they make their way across a network connection is never a bad idea.



Compression

One last quick optimization is compression. If bandwidth is a concern or you’re cost-conscious, you can implement a lossless compression and decompression scheme when you send and receive data from Redis. Here’s an example using the bzip2 compression algorithm, which in this extreme case cuts down on the number of bytes sent across the connection by a factor of over 2,000:

>>>
 1>>> import bz2
 2
 3>>> blob = "i have a lot to talk about" * 10000
 4>>> len(blob.encode("utf-8"))
 5260000
 6
 7>>> # Set the compressed string as value
 8>>> r.set("msg:500", bz2.compress(blob.encode("utf-8")))
 9>>> r.get("msg:500")
10b'BZh91AY&SY\xdaM\x1eu\x01\x11o\x91\x80@\x002l\x87\'  # ... [truncated]
11>>> len(r.get("msg:500"))
12122
13>>> 260_000 / 122  # Magnitude of savings
142131.1475409836066
15
16>>> # Get and decompress the value, then confirm it's equal to the original
17>>> rblob = bz2.decompress(r.get("msg:500")).decode("utf-8")
18>>> rblob == blob
19True

The way that serialization, encryption, and compression are related here is that they all occur client-side. You do some operation on the original object on the client-side that ends up making more efficient use of Redis once you send the string over to the server. The inverse operation then happens again on the client side when you request whatever it was that you sent to the server in the first place.




Using Hiredis

It’s common for a client library such as redis-py to follow a protocol in how it is built. In this case, redis-py implements the REdis Serialization Protocol, or RESP.

Part of fulfilling this protocol consists of converting some Python object in a raw bytestring, sending it to the Redis server, and parsing the response back into an intelligible Python object.

For example, the string response “OK” would come back as "+OK\r\n" , while the integer response 1000 would come back as ":1000\r\n" . This can get more complex with other data types such as RESP arrays.

A parser is a tool in the request-response cycle that interprets this raw response and crafts it into something recognizable to the client. redis-py ships with its own parser class, PythonParser , which does the parsing in pure Python. (See .read_response() if you’re curious.)

However, there’s also a C library, Hiredis, that contains a fast parser that can offer significant speedups for some Redis commands such as LRANGE . You can think of Hiredis as an optional accelerator that it doesn’t hurt to have around in niche cases.

All that you have to do to enable redis-py to use the Hiredis parser is to install its Python bindings in the same environment as redis-py :

$ python -m pip install hiredis

What you’re actually installing here is hiredis-py , which is a Python wrapper for a portion of the hiredis C library.

The nice thing is that you don’t really need to call hiredis dich selbst. Just pip install it, and this will let redis-py see that it’s available and use its HiredisParser instead of PythonParser .

Internally, redis-py will attempt to import hiredis , and use a HiredisParser class to match it, but will fall back to its PythonParser instead, which may be slower in some cases:

# redis/utils.py
try:
    import hiredis
    HIREDIS_AVAILABLE = True
except ImportError:
    HIREDIS_AVAILABLE = False


# redis/connection.py
if HIREDIS_AVAILABLE:
    DefaultParser = HiredisParser
else:
    DefaultParser = PythonParser


Using Enterprise Redis Applications

While Redis itself is open-source and free, several managed services have sprung up that offer a data store with Redis as the core and some additional features built on top of the open-source Redis server:

  • Amazon ElastiCache for Redis : This is a web service that lets you host a Redis server in the cloud, which you can connect to from an Amazon EC2 instance. For full setup instructions, you can walk through Amazon’s ElastiCache for Redis launch page.

  • Microsoft’s Azure Cache for Redis : This is another capable enterprise-grade service that lets you set up a customizable, secure Redis instance in the cloud.

The designs of the two have some commonalities. You typically specify a custom name for your cache, which is embedded as part of a DNS name, such as demo.abcdef.xz.0009.use1.cache.amazonaws.com (AWS) or demo.redis.cache.windows.net (Azure).

Once you’re set up, here are a few quick tips on how to connect.

From the command line, it’s largely the same as in our earlier examples, but you’ll need to specify a host with the h flag rather than using the default localhost. For Amazon AWS , execute the following from your instance shell:

$ export REDIS_ENDPOINT="demo.abcdef.xz.0009.use1.cache.amazonaws.com"
$ redis-cli -h $REDIS_ENDPOINT

For Microsoft Azure , you can use a similar call. Azure Cache for Redis uses SSL (port 6380) by default rather than port 6379, allowing for encrypted communication to and from Redis, which can’t be said of TCP. All that you’ll need to supply in addition is a non-default port and access key:

$ export REDIS_ENDPOINT="demo.redis.cache.windows.net"
$ redis-cli -h $REDIS_ENDPOINT -p 6380 -a <primary-access-key>

The -h flag specifies a host, which as you’ve seen is 127.0.0.1 (localhost) by default.

When you’re using redis-py in Python, it’s always a good idea to keep sensitive variables out of Python scripts themselves, and to be careful about what read and write permissions you afford those files. The Python version would look like this:

>>>
>>> import os
>>> import redis

>>> # Specify a DNS endpoint instead of the default localhost
>>> os.environ["REDIS_ENDPOINT"]
'demo.abcdef.xz.0009.use1.cache.amazonaws.com'
>>> r = redis.Redis(host=os.environ["REDIS_ENDPOINT"])

That’s all there is to it. Besides specifying a different host , you can now call command-related methods such as r.get() as normal.

Note :If you want to use solely the combination of redis-py and an AWS or Azure Redis instance, then you don’t really need to install and make Redis itself locally on your machine, since you don’t need either redis-cli or redis-server .

If you’re deploying a medium- to large-scale production application where Redis plays a key role, then going with AWS or Azure’s service solutions can be a scalable, cost-effective, and security-conscious way to operate.



Wrapping Up

That concludes our whirlwind tour of accessing Redis through Python, including installing and using the Redis REPL connected to a Redis server and using redis-py in real-life examples. Here’s some of what you learned:

  • redis-py lets you do (almost) everything that you can do with the Redis CLI through an intuitive Python API.
  • Mastering topics such as persistence, serialization, encryption, and compression lets you use Redis to its full potential.
  • Redis transactions and pipelines are essential parts of the library in more complex situations.
  • Enterprise-level Redis services can help you smoothly use Redis in production.

Redis has an extensive set of features, some of which we didn’t really get to cover here, including server-side Lua scripting, sharding, and master-slave replication. If you think that Redis is up your alley, then make sure to follow developments as it implements an updated protocol, RESP3.



Further Reading

Here are some resources that you can check out to learn more.

Books:

  • Josiah Carlson: Redis in Action
  • Karl Seguin: The Little Redis Book
  • Luc Perkins et. al.: Seven Databases in Seven Weeks

Redis in use:

  • Twitter: Real-Time Delivery Architecture at Twitter
  • Spool: Redis bitmaps – Fast, easy, realtime metrics
  • 3scale: Having fun with Redis Replication between Amazon and Rackspace
  • Instagram: Storing hundreds of millions of simple key-value pairs in Redis
  • Craigslist: Redis Sharding at Craigslist
  • Disqus: Redis at Disqus

Other:

  • Digital Ocean: How To Secure Your Redis Installation
  • AWS: ElastiCache for Redis User Guide
  • Microsoft: Azure Cache for Redis
  • Cheatography: Redis Cheat Sheet
  • ClassDojo: Better Rate Limiting With Redis Sorted Sets
  • antirez (Salvatore Sanfilippo): Redis persistence demystified
  • Martin Kleppmann: How to do distributed locking
  • HighScalability: 11 Common Web Use Cases Solved in Redis