Comment utiliser Redis avec Python – Real Python

By | juillet 1, 2019

Formation gratuite Python

Dans ce didacticiel, vous apprendrez à utiliser Python avec Redis (prononcé RED-iss, ou peut-être REE-diss ou Red-DEES, selon votre interlocuteur), qui est un magasin de valeurs-clés en mémoire extrêmement rapide qui peut être utilisé pour n'importe quoi de A à Z. Voici ce que Sept bases de données en sept semaines, un livre populaire sur les bases de données, dit à propos de Redis:

Ce n’est pas simplement facile à utiliser; c’est une joie. Si une API est UX pour les programmeurs, Redis devrait figurer dans le Museum of Modern Art, à côté du Mac Cube.

Et en termes de vitesse, Redis est difficile à battre. Les lectures sont rapides et les écritures sont encore plus rapides, avec plus de 100 000 manipulations ENSEMBLE opérations par seconde par certains points de repère. (La source)

Intrigué? Ce tutoriel est conçu pour le programmeur Python qui peut avoir une expérience de zéro à petite expérience de Redis. Nous aborderons deux outils à la fois et présenterons à la fois Redis et l’une de ses bibliothèques clientes Python, Redis-py.

Redis-py (que vous importez comme juste redis) est l’un des nombreux clients Python pour Redis, mais il a la particularité d’être présenté comme «la voie à suivre pour Python» par les développeurs Redis eux-mêmes. Il vous permet d'appeler des commandes Redis depuis Python et de récupérer des objets Python familiers en retour.

Dans ce tutoriel, vous allez couvrir:

  • Installer Redis à partir des sources et comprendre le but des fichiers binaires résultants
  • Apprendre une petite portion de Redis elle-même, y compris sa syntaxe, son protocole et son design
  • Maîtriser Redis-py tout en voyant un aperçu de la manière dont il met en œuvre le protocole de Redis
  • Configuration et communication avec une instance de serveur Amazon ElastiCache Redis

Installer Redis à partir de la source

Comme l’a dit mon arrière-arrière-grand-père, rien n’est plus grave que l’installation à partir de la source. Cette section vous guidera à travers le téléchargement, la création et l’installation de Redis. Je vous promets que cela ne fera pas mal du tout!

Commencez par télécharger le code source Redis sous forme d'archive:

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

Ensuite, passez à racine et extraire le code source de l’archive pour / 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

Facultativement, vous pouvez maintenant supprimer l'archive elle-même:

Cela vous laissera avec un dépôt de code source à / usr / local / lib / redis-stable /. Redis est écrit en C, vous devez donc compiler, lier et installer avec le logiciel faire utilitaire:

$ CD / usr / local / lib / redis-stable /
$ faire && faire installer

En utilisant faire installer fait deux actions:

  1. La première faire La commande compile et lie le code source.

  2. le faire installer une partie prend les binaires et les copie / usr / local / bin / afin que vous puissiez les exécuter de n'importe où (en supposant que / usr / local / bin / est dans CHEMIN).

Voici toutes les étapes jusqu'à présent:

$ 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 /
$ faire && faire installer

À ce stade, prenez un moment pour confirmer que Redis est à vos côtés. CHEMIN et vérifier sa version:

$ redis-cli --version
redis-cli 5.0.3

Si votre shell ne peut pas trouver redis-cli, assurez-vous que / usr / local / bin / est sur votre CHEMIN variable d'environnement, et l'ajouter sinon.

En plus de redis-cli, faire installer conduit en fait à une poignée de fichiers exécutables différents (et un lien symbolique) placés à / usr / local / bin /:

$ # Un instantané des exécutables fournis avec Redis
$ ls -hFG / usr / local / bin / redis- * | Trier
/ 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 *

Bien que tous aient un usage prévu, les deux qui vous intéresseront le plus sont: redis-cli et serveur redis, que nous décrirons bientôt. Mais avant d’y arriver, il est bon de configurer une configuration de base.

Configuration de Redis

Redis est hautement configurable. Prenons une minute pour définir certaines options de configuration épurées relatives à la persistance de la base de données et à la sécurité de base:

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

Maintenant, écrivez ce qui suit à /etc/redis/6379.conf. Nous couvrirons ce que la plupart d’entre elles signifient progressivement tout au long du didacticiel:

# /etc/redis/6379.conf

port 6379
démoniser oui
économisez 60 1
lier 127.0.0.1
tcp-keepalive 300
dbfilename dump.rdb
dir ./
rdbcompression oui

La configuration de Redis est auto-documentée, avec l'exemple redis.conf fichier situé dans la source Redis pour votre plus grand plaisir. Si vous utilisez Redis dans un système de production, il est utile de supprimer toutes les sources de distraction et de prendre le temps de lire cet exemple de fichier dans son intégralité pour vous familiariser avec les rouages ​​de Redis et affiner votre configuration.

Certains tutoriels, y compris des parties de la documentation de Redis, peuvent également suggérer d’exécuter le script Shell. install_server.sh situé dans redis / utils / install_server.sh. Nous vous invitons à utiliser cette option comme une alternative plus complète à ce qui précède, mais prenez note de quelques points plus précis concernant install_server.sh:

  • Cela ne fonctionnera pas sous Mac OS X – uniquement sous Debian et Ubuntu Linux.
  • Il injectera un ensemble plus complet d’options de configuration dans /etc/redis/6379.conf.
  • Il va écrire un System V init script à /etc/init.d/redis_6379 ça vous laissera faire sudo service redis_6379 start.

Le guide de démarrage rapide de Redis contient également une section sur une configuration plus appropriée de Redis, mais les options de configuration ci-dessus devraient être totalement suffisantes pour ce didacticiel et sa mise en route.

Cela fait, nous pouvons maintenant utiliser Redis lui-même.

Dix minutes ou plus à Redis

Cette section vous fournira juste assez de connaissances sur Redis pour être dangereux, décrivant sa conception et son utilisation de base.

Commencer

Redis a un architecture client-serveur et utilise un modèle demande-réponse. Cela signifie que vous (le client) vous connectez à un serveur Redis via une connexion TCP, sur le port 6379 par défaut. Vous demandez une action (comme une forme de lecture, d’écriture, d’obtention, de réglage ou de mise à jour) et le serveur sert vous sauvegardez une réponse.

De nombreux clients peuvent communiquer avec le même serveur, ce qui est vraiment ce que Redis ou toute application client-serveur est. Chaque client effectue une lecture (généralement bloquante) sur un socket en attente de la réponse du serveur.

le cli dans redis-cli représente interface de ligne de commande, et le serveur dans serveur redis est pour, eh bien, exécuter un serveur. De la même manière que vous courriez python en ligne de commande, vous pouvez exécuter redis-cli pour sauter dans une boucle REPL (Read Eval Print Loop) interactive où vous pouvez exécuter des commandes client directement à partir du shell.

Tout d’abord, cependant, vous devrez lancer serveur redis de sorte que vous ayez un serveur Redis en cours avec lequel parler. Une façon courante de le faire en développement est de démarrer un serveur sur localhost (adresse IPv4 127.0.0.1), qui est la valeur par défaut, sauf indication contraire de Redis. Vous pouvez aussi passer serveur redis le nom de votre fichier de configuration, ce qui revient à spécifier toutes ses paires clé-valeur en tant qu'arguments de ligne de commande:

$ serveur redis /etc/redis/6379.conf
31829: C 07 mars 2019 08: 45: 04.030 # oO0OoO0OoO0Oo Redis commence oO0OoO0OoO0Oo
31829: C 07 mars 2019 08: 45: 04.030 # Version de Redis = 5.0.3, bits = 64, commit = 00000000, modifié = 0, pid = 31829, vient de commencer
31829: C 07 mars 2019 08: 45: 04.030 # Configuration chargée

Nous avons mis le démoniser option de configuration pour Oui, le serveur s’exécute donc en arrière-plan. (Sinon, utilisez --démoniser oui en option à serveur redis.)

Vous êtes maintenant prêt à lancer Redis REPL. Entrer redis-cli sur votre ligne de commande. Vous verrez le serveur port hôte paire suivie d'un > rapide:

Voici l'une des commandes les plus simples de Redis, PING, qui teste simplement la connectivité au serveur et renvoie "PONG" si tout va bien:

127.0.0.1:6379> PING
PONG

Les commandes Redis ne font pas la distinction entre les majuscules et les minuscules, bien que leurs homologues en Python ne le soient absolument pas.

Ensuite, nous utiliserons certaines des commandes Redis courantes et les comparerons à ce qu’elles seraient en Python pur.

Redis comme un dictionnaire Python

Redis représente Service de dictionnaire distant.

"Vous voulez dire, comme un dictionnaire Python?", Vous pouvez demander.

Oui. De manière générale, il existe de nombreux parallèles que vous pouvez établir entre un dictionnaire Python (ou une table de hachage générique) et ce que Redis est et fait:

  • Une base de données Redis détient valeur clé paires et prend en charge des commandes telles que OBTENIR, ENSEMBLE, et DEL, ainsi que plusieurs centaines de commandes supplémentaires.

  • Redis clés sont toujours des chaînes.

  • Redis valeurs peut être un certain nombre de types de données différents. Nous allons couvrir certains des types de données de valeur les plus essentiels de ce tutoriel: chaîne, liste, hachage, et ensembles. Certains types avancés incluent les éléments géospatiaux et le nouveau type de flux.

  • De nombreuses commandes Redis fonctionnent en temps constant O (1), tout comme pour récupérer une valeur d’un fichier Python. dict ou n'importe quelle table de hachage.

Le créateur de Redis, Salvatore Sanfilippo, n’aimerait probablement pas la comparaison d’une base de données Redis avec un Python ordinaire dict. Il appelle le projet un «serveur de structure de données» (plutôt qu'un magasin de valeurs-clés, tel que memcached), car Redis prend en charge le stockage de types de données supplémentaires. valeur clé types de données en plus chaîne: chaîne. Mais pour notre propos, c’est une comparaison utile si vous connaissez l’objet dictionnaire de Python.

Entrons et apprenons par l'exemple. Notre première base de données de jouets (identifiant 0) sera une cartographie de pays: capitale, où nous utilisons ENSEMBLE pour définir des paires clé-valeur:

127.0.0.1:6379> ENSEMBLE Bahamas Nassau
D'accord
127.0.0.1:6379> ENSEMBLE Croatie Zagreb
D'accord
127.0.0.1:6379> OBTENIR Croatie
"Zagreb"
127.0.0.1:6379> OBTENIR Japon
(néant)

La séquence d'instructions correspondante en pur Python ressemblerait à ceci:

>>>

>>> capitales = 
>>> capitales[[[["Bahamas"] = "Nassau"
>>> capitales[[[["Croatie"] = "Zagreb"
>>> capitales.obtenir("Croatie")
'Zagreb'
>>> capitales.obtenir("Japon")  # Aucun

Nous utilisons capitals.get ("Japon") plutôt que capitales["Japan"] parce que Redis reviendra néant lorsqu’une clé n’est pas trouvée, ce qui est analogue à celui de Python. Aucun.

Redis vous permet également de définir et d’obtenir plusieurs paires clé-valeur en une seule commande, MSET et MGET, respectivement:

127.0.0.1:6379> MSET Liban Beyrouth Norvège Oslo France Paris
D'accord
127.0.0.1:6379> MGET Liban Norvège Bahamas
1) "Beyrouth"
2) "Oslo"
3) "Nassau"

La chose la plus proche en Python est avec dict.update ():

>>>

>>> capitales.mettre à jour(
...     "Liban": "Beyrouth",
...     "Norvège": "Oslo",
...     "France": "Paris",
... )
>>> [[[[capitales[[[[k] pour k dans ("Liban", "Norvège", "Bahamas")]
['Beirut', 'Oslo', 'Nassau']

Comme troisième exemple, le EXISTE commande fait ce que cela ressemble, à savoir vérifier si une clé existe:

127.0.0.1:6379> EXISTE Norvège
(entier) 1
127.0.0.1:6379> EXISTE Suède
(entier) 0

Python a le dans mot-clé pour tester la même chose, qui route vers dict .__ contient __ (clé):

>>>

>>> "Norvège" dans capitales
Vrai
>>> "Suède" dans capitales
Faux

Ces quelques exemples ont pour but de montrer, à l’aide de Python natif, ce qui se passe à un niveau élevé avec quelques commandes Redis courantes. Il n’existe aucun composant client-serveur dans les exemples Python, et Redis-py n'est pas encore entré dans l'image. Cela n'a pour but que de montrer la fonctionnalité de Redis par exemple.

Voici un résumé des quelques commandes Redis que vous avez vues et de leurs équivalents Python fonctionnels:

capitales[[[["Bahamas"] = "Nassau"
capitales.mettre à jour(
    
        "Liban": "Beyrouth",
        "Norvège": "Oslo",
        "France": "Paris",
    
)
[[[[capitales[[[[k] pour k dans ("Liban", "Norvège", "Bahamas")]

La bibliothèque cliente Python Redis, Redis-py, dans lequel vous allez plonger dans cet article, fait les choses différemment. Il encapsule une connexion TCP réelle sur un serveur Redis et envoie au serveur les commandes brutes, telles que les octets sérialisés à l'aide du protocole de sérialisation REdis (RESP). Il prend ensuite la réponse brute et la analyse dans un objet Python tel que octets, int, ou même datetime.datetime.

Jusqu’à présent, vous avez vu quelques-uns des types de données fondamentaux de Redis, à savoir un mappage de chaîne: chaîne. Bien que cette paire clé-valeur soit commune à la plupart des magasins de clé-valeur, Redis propose un certain nombre d'autres types de valeur possibles, que vous verrez ensuite.

Autres types de données dans Python vs Redis

Avant de lancer le Redis-py Client Python, il est également utile d’avoir une connaissance de base de quelques types de données Redis supplémentaires. Pour être clair, toutes les clés Redis sont des chaînes. C’est la valeur qui peut prendre des types de données (ou des structures) en plus des valeurs de chaîne utilisées jusqu’à présent dans les exemples.

UNE hacher est une cartographie de chaîne: chaîne, appelé valeur de champ paires, qui se trouve sous une clé de niveau supérieur:

127.0.0.1:6379> HSET realpython url "https://realpython.com/"
(entier) 1
127.0.0.1:6379> HSET realpython github realpython
(entier) 1
127.0.0.1:6379> HSET realpython nom complet "Real Python"
(entier) 1

Ceci définit trois paires champ-valeur pour un clé, "realpython". Si vous êtes habitué à la terminologie et aux objets Python, cela peut prêter à confusion. Un hachage Redis est à peu près analogue à un Python dict qui est imbriqué un niveau de profondeur:

Les données = 
    "realpython": 
        "url": "https://realpython.com/",
        "github": "realpython",
        "nom complet": "Vrai python",
    

Les champs de Redis ressemblent aux clés Python de chaque paire clé-valeur imbriquée dans le dictionnaire interne ci-dessus. Redis réserve le terme clé pour la clé de base de données de niveau supérieur contenant la structure de hachage elle-même.

Juste comme il y a MSET pour base chaîne: chaîne paires clé-valeur, il y a aussi HMSET pour que les hachages définissent plusieurs paires dans l'objet de valeur de hachage:

127.0.0.1:6379> HMSET pypa url "https://www.pypa.io/" github pypa nom complet "Python Packaging Authority"
D'accord
127.0.0.1:6379> HGETALL pypa
1) "url"
2) "https://www.pypa.io/"
3) "github"
4) "pypa"
5) "nom complet"
6) "Python Packaging Authority"

En utilisant HMSET est probablement un parallèle plus étroit pour la façon dont nous avons attribué Les données dictionnaire imbriqué ci-dessus, plutôt que de définir chaque paire imbriquée comme cela est fait avec HSET.

Deux types de valeur supplémentaires sont des listes et ensembles, qui peut remplacer un hachage ou une chaîne en tant que valeur Redis. Ils ressemblent en grande partie à ce qu’ils ressemblent, je ne vais donc pas prendre votre temps avec d’autres exemples. Les hachages, les listes et les ensembles ont chacun des commandes spécifiques à ce type de données, qui sont parfois signalées par leur lettre initiale:

  • Hachage: Les commandes à utiliser sur les hashes commencent par un H, tel que HSET, HGET, ou HMSET.

  • Ensembles: Les commandes pour opérer sur les ensembles commencent par un S, tel que SCARD, qui obtient le nombre d’éléments à la valeur définie correspondant à une clé donnée.

  • Listes: Les commandes pour opérer sur les listes commencent par un L ou R. Les exemples comprennent LPOP et RPUSH. le L ou R se réfère à quel côté de la liste est opéré. Quelques commandes de liste sont également précédées d’un B, ce qui signifie blocage. Une opération de blocage ne laisse pas les autres opérations l’interrompre pendant son exécution. Par exemple, BLPOP exécute un blocage gauche-pop sur une structure de liste.

Voici une liste rapide des commandes spécifiques aux types de données string, hash, list et set dans Redis:

Type Les commandes
Ensembles SADD, SCARD, SDIFF, SDIFFSTORE, SINTER, SINTERSTORE, SISMEMBRE, SMEMBERS, SMOVE, SPOP, SRANDMEMBER, SREM, SSCAN, SUNION, SUNIONSTORE
Des hachis HDEL, HEXISTES, HGET, HGETALL, HINCRBY, HINCRBYFLOAT, HKEYS, HLEN, HMGET, HMSET, HSCAN, HSET, HSETNX, HSTRLEN, HVALS
Des listes BLPOP, BRPOP, BRPOPLPUSH, LINDEX, LINSERT, LLEN, LPOP, LPUSH, LPUSHX, LRANGE, LREM, LSET, LTRIM, RPOP, RPOPLPUSH, RPUSH, RPUSHX
Les cordes AJOUTER, BITCOUNT, BITFIELD, BITOP, BITPOS, DECR, DECRBY, OBTENIR, GETBIT, GETRANGE, SE METTRE, INCR, INCRBY, INCRBYFLOAT, MGET, MSET, MSETNX, PSETEX, ENSEMBLE, SETBIT, SETEX, SETNX, SETRANGE, STRLEN

Ce tableau n’est pas une image complète des commandes et des types Redis. Il existe toute une gamme de types de données plus avancés, tels que les éléments géospatiaux, les ensembles triés et HyperLogLog. Sur la page des commandes Redis, vous pouvez filtrer par groupe de structure de données. Il y a aussi le résumé des types de données et l'introduction aux types de données Redis.

Puisque nous allons passer à l’utilisation de Python, vous pouvez maintenant effacer votre base de données de jouets avec FLUSHDB et quitter le redis-cli REPL:

127.0.0.1:6379> FLUSHDB
D'accord
127.0.0.1:6379> QUITTER

Cela vous ramènera à votre invite de shell. Tu peux partir serveur redis fonctionne en arrière-plan, car vous en aurez également besoin pour le reste du didacticiel.

En utilisant Redis-py: Redis en Python

Maintenant que vous maîtrisez les bases de Redis, il est temps de vous lancer Redis-py, le client Python qui vous permet de parler à Redis à partir d’une API conviviale Python.

Premiers pas

Redis-py est une bibliothèque client Python bien établie qui vous permet de parler directement à un serveur Redis via des appels Python:

$ python -m pip installer redis

Ensuite, assurez-vous que votre serveur Redis est toujours opérationnel en arrière-plan. Vous pouvez vérifier avec pgrep redis-serveret si vous arrivez les mains vides, redémarrez un serveur local avec serveur redis /etc/redis/6379.conf.

Passons maintenant à la partie centrée sur Python. Voici le «monde bonjour» de Redis-py:

>>>

    1 >>> importation redis
    2 >>> r = redis.Redis()
    3 >>> r.mset("Croatie": "Zagreb", "Bahamas": "Nassau")
    4 Vrai
    5 >>> r.obtenir("Bahamas")
    6 b'Nassau '

Redis, utilisé dans la ligne 2, est la classe centrale du paquet et le bourreau de travail par lequel vous exécutez (presque) n’importe quelle commande Redis. La connexion de socket TCP et la réutilisation est faite pour vous dans les coulisses, et vous appelez les commandes Redis en utilisant des méthodes sur l’instance de la classe r.

Notez également que le type de l'objet retourné, b'Nassau ' dans la ligne 6, est-ce que Python octets type, pas str. Il est octets plutôt que str c'est le type de retour le plus courant sur Redis-py, alors vous devrez peut-être appeler r.get ("Bahamas"). decode ("utf-8") en fonction de ce que vous voulez réellement faire avec la chaîne d'octets retournée.

Le code ci-dessus vous semble-t-il familier? Les méthodes dans presque tous les cas correspondent au nom de la commande Redis qui fait la même chose. Ici, vous avez appelé r.mset () et r.get (), qui correspondent à MSET et OBTENIR dans l'API Redis native.

Cela signifie aussi que HGETALL devient r.hgetall (), PING devient r.ping (), etc. Il existe quelques exceptions, mais la règle est valable pour la grande majorité des commandes.

Bien que les arguments de la commande Redis se traduisent généralement par une signature de méthode d'aspect similaire, ils utilisent des objets Python. Par exemple, l'appel à r.mset () dans l'exemple ci-dessus utilise un Python dict comme premier argument plutôt que comme une suite de chaînes d'octets.

Nous avons construit le Redis exemple r sans arguments, mais il est livré avec un certain nombre de paramètres si vous en avez besoin:

# De redis / client.py
classe Redis(objet):
    def __init__(soi, hôte='localhost', Port=6379,
                 db=0, mot de passe=Aucun, socket_timeout=Aucun,
                 # ...

Vous pouvez voir que la valeur par défaut nom d'hôte: port la paire est localhost: 6379, ce qui est exactement ce dont nous avons besoin dans le cas de nos locaux serveur redis exemple.

le db paramètre est le numéro de base de données. Vous pouvez gérer plusieurs bases de données simultanément, chacune d’elles étant identifiée par un entier. Le nombre maximal de bases de données est 16 par défaut.

Quand tu cours juste redis-cli à partir de la ligne de commande, cela vous commence à la base de données 0. Utilisez le -n flag pour démarrer une nouvelle base de données, comme dans redis-cli -n 5.

Types de clé autorisés

Une chose à savoir, c’est que Redis-py exige que vous passiez les clés qui sont octets, str, int, ou flotte. (Il convertira les 3 derniers de ces types en octets avant de les envoyer au serveur.)

Prenons le cas où vous souhaitez utiliser les dates du calendrier comme clés:

>>>

>>> importation date / heure
>>> aujourd'hui = date / heure.rendez-vous amoureux.aujourd'hui()
>>> visiteurs = "dan", "jon", "alex"
>>> r.sadd(aujourd'hui, *visiteurs)
Traceback (dernier appel le plus récent):
# ...
redis.exceptions.DataError: Entrée de type non valide: 'date'.
Commencez par convertir en octet, chaîne ou numéro.

Vous devrez explicitement convertir le fichier Python. rendez-vous amoureux objecter à str, que vous pouvez faire avec .isoformat ():

>>>

>>> stoday = aujourd'hui.isoformat()  # Python 3.7+, ou utilisez str (aujourd'hui)
>>> stoday
'2019-03-10'
>>> r.sadd(stoday, *visiteurs)  # sadd: set-add
3
>>> r.smembers(stoday)
b'dan ', b'alex', b'jon '
>>> r.écarter(aujourd'hui.isoformat())
3

Pour récapituler, Redis lui-même autorise uniquement les chaînes comme clés. Redis-py Il accepte un peu plus les types Python, bien qu’il convertisse finalement tout en octets avant de les envoyer à un serveur Redis.

Exemple: PyHats.com

Il est temps de donner un exemple plus complet. Imaginons que nous avons décidé de créer un site Web lucratif, PyHats.com, qui vend des chapeaux excessivement chers à quiconque les achètera et qui vous a embauché pour construire ce site.

Vous utiliserez Redis pour gérer une partie du catalogue de produits, l’inventaire et la détection du trafic de bot pour PyHats.com.

C’est le premier jour du site et nous allons vendre trois chapeaux à édition limitée. Chaque chapeau est maintenu dans un hachage Redis de paires champ-valeur, et le hachage a une clé qui est un entier aléatoire préfixé, tel que chapeau: 56854717. En utilisant le chapeau: Le préfixe est la convention Redis pour la création d'une sorte d'espace de noms dans une base de données Redis:

importation au hasard

au hasard.la graine(444)
Chapeaux = F"chapeau: random.getrandbits (32)": je pour je dans (
    
        "Couleur": "noir",
        "prix": 49.99,
        "style": "équipé",
        "quantité": 1000,
        "npurchased": 0,
    ,
    
        "Couleur": "bordeaux",
        "prix": 59.99,
        "style": "branché",
        "quantité": 500,
        "npurchased": 0,
    ,
    
        "Couleur": "vert",
        "prix": 99.99,
        "style": "base-ball",
        "quantité": 200,
        "npurchased": 0,
    )

Commençons par la base de données 1 depuis que nous avons utilisé la base de données 0 dans un exemple précédent:

>>>

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

Pour faire une première écriture de ces données dans Redis, nous pouvons utiliser .hmset () (hash multi-set), en l'appelant pour chaque dictionnaire. Le «multi» fait référence à la définition de plusieurs paires champ-valeur, où «champ» correspond dans ce cas à une clé de l'un des dictionnaires imbriqués dans Chapeaux:

    1 >>> avec r.pipeline() comme tuyau:
    2 ...    pour h_id, chapeau dans Chapeaux:
    3 ...        tuyau.hmset(h_id, chapeau)
    4 ...    tuyau.exécuter()
    5 Pipeline<ConnectionPool<Lien<hôte=localhost,Port=6379,db=0>>>
    6 Pipeline<ConnectionPool<Lien<hôte=localhost,Port=6379,db=0>>>
    7 Pipeline<ConnectionPool<Lien<hôte=localhost,Port=6379,db=0>>>
    8 [[[[Vrai, Vrai, Vrai]
    9 
dix >>> r.Bgsave()
11 Vrai

Le bloc de code ci-dessus introduit également le concept de Redis pipeline, qui permet de réduire le nombre de transactions aller-retour nécessaires pour écrire ou lire des données à partir de votre serveur Redis. Si tu venais d'appeler r.hmset () trois fois, cela nécessiterait alors une opération aller-retour pour chaque ligne écrite.

Avec un pipeline, toutes les commandes sont mises en mémoire tampon côté client, puis envoyées en une fois, en un seul coup, en utilisant pipe.hmset () ligne 3. C’est pourquoi les trois Vrai les réponses sont toutes renvoyées en même temps, lorsque vous appelez pipe.execute () ligne 4. Vous verrez bientôt un scénario d’utilisation plus avancé pour un pipeline.

Faisons une brève vérification de la présence de tout dans notre base de données Redis:

>>>

>>> empreinte(r.Hgetall("chapeau: 56854717"))
b'color ': b'green',
    b'npurchased ': b'0',
    b'price ': b'99.99',
    b'quantity ': b'200',
    b'style ': b'baseball'

>>> r.clés()  # Attention sur un gros DB. keys () est O (N)
[b'56854717', b'1236154736', b'1326692461']

La première chose que nous voulons simuler est ce qui se passe lorsqu'un utilisateur clique achat. Si l'article est en stock, augmentez-le racheté par 1 et diminuer sa quantité (inventaire) par 1. Vous pouvez utiliser .hincrby () pour faire ça:

>>>

>>> r.hincrby("chapeau: 56854717", "quantité", -1)
199
>>> r.hget("chapeau: 56854717", "quantité")
b'199 '
>>> r.hincrby("chapeau: 56854717", "npurchased", 1)
1

Ce n’est pas si simple, cependant. Changer le quantité et racheté en deux lignes de code cache la réalité qu'un clic, un achat et un paiement impliquent plus que cela. Nous devons faire quelques vérifications supplémentaires pour nous assurer de ne pas laisser quelqu'un avec un portefeuille plus léger et sans chapeau:

  • Étape 1: Vérifiez si l'article est en stock, ou sinon, déclenchez une exception sur le backend.
  • Étape 2: S'il est en stock, alors exécutez la transaction, diminuez le quantité champ, et augmenter la racheté champ.
  • Étape 3: Soyez attentif aux modifications qui modifient l'inventaire entre les deux premières étapes (condition de concurrence critique).

L’étape 1 est relativement simple: elle consiste en une .hget () pour vérifier la quantité disponible.

L'étape 2 est un peu plus compliquée. La paire d’opérations d’augmentation et de diminution doit être exécutée atomiquement: soit les deux doivent être complétés avec succès, soit aucun des deux ne doit l'être (dans le cas où au moins un échoue).

Avec les infrastructures client-serveur, il est toujours crucial de faire attention à l’attricité et de rechercher ce qui peut ne pas tourner mal si plusieurs clients essaient de parler au serveur en même temps. La réponse à cela dans Redis est d’utiliser un transaction block, ce qui signifie que les deux ou aucune des commandes passent à travers.

Dans Redis-py, Pipeline est un pipeline transactionnel classe par défaut. Cela signifie que, même si la classe porte en réalité un autre nom (pipeline), elle peut également être utilisée pour créer un bloc de transaction.

Dans Redis, une transaction commence par MULTI et se termine par EXEC:

    1 127.0.0.1:6379> MULTI
    2 127.0.0.1:6379> HINCRBY 56854717 quantité -1
 3 127.0.0.1:6379> HINCRBY 56854717 npurchased 1
 4 127.0.0.1:6379> EXEC

MULTI (Ligne 1) marque le début de la transaction, et EXEC (Ligne 4) marque la fin. Tout ce qui est entre les deux est exécuté sous la forme d'une séquence de commandes tamponnée tout ou rien. Cela signifie qu'il sera impossible de décrémenter quantité (Ligne 2) mais avoir ensuite l’équilibrage racheté échec de l'opération d'incrémentation (ligne 3).

Revenons à l’étape 3: nous devons être conscients de tout changement modifiant l’inventaire entre les deux premières étapes.

L'étape 3 est la plus délicate. Disons qu’il ne reste qu’un chapeau isolé dans notre inventaire. Entre le moment où l'utilisateur A vérifie la quantité de chapeaux restants et traite effectivement leur transaction, l'utilisateur B vérifie également l'inventaire et constate de la même manière qu'un chapeau est en stock. Les deux utilisateurs seront autorisés à acheter le chapeau, mais nous avons un chapeau à vendre, pas deux; nous sommes donc raccrochés et un utilisateur n’a plus d’argent. Pas bon.

Redis a une réponse intelligente au problème de l’étape 3: il s’appelle verrouillage optimiste, et diffère de la manière dont fonctionne le verrouillage typique dans un SGBDR tel que PostgreSQL. En résumé, le verrouillage optimiste signifie que la fonction appelante (client) n’acquiert pas de verrou, mais surveille plutôt l’évolution des données sur lesquelles elle écrit. pendant le temps, il aurait tenu un verrou. En cas de conflit pendant cette période, la fonction d’appel réessaie simplement l’ensemble du processus.

Vous pouvez effectuer un verrouillage optimiste en utilisant le REGARDER commande (.regarder() dans Redis-py), qui fournit un check-and-set comportement.

Introduisons un gros morceau de code et parcourons-le ensuite, étape par étape. Vous pouvez imaginer acheter un article() comme étant appelé chaque fois qu'un utilisateur clique sur un Acheter maintenant ou achat bouton. Son but est de confirmer que l'article est en stock et de prendre une mesure en fonction de ce résultat, le tout de manière sûre, en tenant compte des conditions de concurrence et en essayant de nouveau, le cas échéant:

    1 importation enregistrement
    2 importation redis
    3 
    4 enregistrement.basicConfig()
    5 
    6 classe OutOfStockError(Exception):
    7     "" "Elevé quand PyHats.com est sorti du chapeau le plus populaire du moment" ""
    8 
    9 def acheter un article(r: redis.Redis, ID de l'article: int) -> Aucun:
dix     avec r.pipeline() comme tuyau:
11         nombre_erreur = 0
12         tandis que Vrai:
13             essayer:
14                 # Obtenir l'inventaire disponible, en surveillant les modifications
15                 # lié à cet itemid avant la transaction
16                 tuyau.regarder(ID de l'article)
17                 rien: octets = r.hget(ID de l'article, "quantité")
18                 si rien > b"0":
19                     tuyau.multi()
20                     tuyau.hincrby(ID de l'article, "quantité", -1)
21                     tuyau.hincrby(ID de l'article, "npurchased", 1)
22                     tuyau.exécuter()
23                     Pause
24                 autre:
25                     # Arrêtez de regarder l'élément et relancez pour sortir
26                     tuyau.décoller()
27                     élever OutOfStockError(
28                         F"Pardon, ID de l'article    Est en rupture de stock!"
29                     )
30             sauf redis.WatchError:
31                 # Log total num. des erreurs de cet utilisateur pour acheter cet article,
32                 # puis essayez à nouveau le même processus de WATCH / HGET / MULTI / EXEC
33                 nombre_erreur + = 1
34                 enregistrement.Attention(
35                     "WatchError #%ré: % s; réessayer ",
36                     nombre_erreur, ID de l'article
37                 )
38     revenir Aucun

La ligne critique se produit à la ligne 16 avec pipe.watch (itemid), qui dit à Redis de surveiller la donnée ID de l'article pour toute modification de sa valeur. Le programme vérifie l'inventaire via l'appel à r.hget (itemid, "quantité"), dans la ligne 17:

16 tuyau.regarder(ID de l'article)
17 rien: octets = r.hget(ID de l'article, "quantité")
18 si rien > b"0":
19     # Objet en stock. Procéder à la transaction.

Si l'inventaire est touché pendant cette courte fenêtre entre le moment où l'utilisateur vérifie le stock et tente de l'acheter, Redis renvoie une erreur et Redis-py va soulever une WatchError (Ligne 30). C’est-à-dire, si l’un des hash pointés par ID de l'article changements après la .hget () appeler mais avant la suivante .hincrby () appels des lignes 20 et 21, nous relancerons alors le processus dans une autre itération du alors que vrai boucle en conséquence.

C’est la partie «optimiste» du verrouillage: au lieu de laisser au client un verrou total sur la base de données qui prend beaucoup de temps lors des opérations d’obtention et de réglage, nous laissons à Redis le soin d’avertir le client et l’utilisateur au cas où demande une nouvelle tentative de vérification de l'inventaire.

Une clé ici est dans la compréhension de la différence entre côté client et du côté serveur opérations:

rien = r.hget(ID de l'article, "quantité")

Cette affectation Python apporte le résultat de r.hget () côté client. Inversement, les méthodes que vous appelez tuyau tamponne efficacement toutes les commandes en une seule, puis les envoie au serveur en une seule requête:

16 tuyau.multi()
17 tuyau.hincrby(ID de l'article, "quantité", -1)
18 tuyau.hincrby(ID de l'article, "npurchased", 1)
19 tuyau.exécuter()

Aucune donnée ne revient au client au milieu du pipeline transactionnel. Vous devez appeler .exécuter() (Ligne 19) pour obtenir la séquence de résultats en une fois.

Même si ce bloc contient deux commandes, il consiste en exactement une opération aller-retour entre client et serveur.

Cela signifie que le client ne peut pas immédiatement utilisation Le résultat de pipe.hincrby (itemid, "quantité", -1), de la ligne 20, car les méthodes sur un Pipeline retourner juste le tuyau exemple lui-même. Nous n’avons encore rien demandé au serveur. Bien que normalement .hincrby () renvoie la valeur obtenue, vous ne pouvez pas la référencer immédiatement côté client tant que la transaction n’est pas terminée.

Il y a un catch-22: c’est aussi pourquoi vous ne pouvez pas passer l’appel à .hget () dans le bloc de transaction. Si vous faites cela, vous ne pourrez pas savoir si vous voulez incrémenter le racheté champ, car vous ne pouvez pas obtenir de résultats en temps réel à partir de commandes insérées dans un pipeline transactionnel.

Enfin, si l’inventaire reste à zéro, alors nous UNWATCH l'ID d'article et soulevez un OutOfStockError (Ligne 27), affichant finalement cette convoitée Épuisé page qui incitera nos acheteurs à acheter encore plus de nos chapeaux à des prix toujours plus farfelus:

24 autre:
25     # Arrêtez de regarder l'élément et relancez pour sortir
26     tuyau.décoller()
27     élever OutOfStockError(
28         F"Pardon, ID de l'article    Est en rupture de stock!"
29     )

Voici une illustration. Gardez à l'esprit que notre quantité de départ est 199 pour chapeau 56854717 depuis que nous avons appelé .hincrby () au dessus de. Imitons 3 achats, ce qui devrait modifier le quantité et racheté des champs:

>>>

>>> acheter un article(r, "chapeau: 56854717")
>>> acheter un article(r, "chapeau: 56854717")
>>> acheter un article(r, "chapeau: 56854717")
>>> r.hmget("chapeau: 56854717", "quantité", "npurchased")  # Hash multi-get
[b'196', b'4']

Nous pouvons désormais effectuer rapidement des achats supplémentaires, en imitant un flux d’achats jusqu’à épuisement total du stock. Encore une fois, imaginez que ceux-ci proviennent d’un grand nombre de clients plutôt que d’un seul. Redis exemple:

>>>

>>> # Achetez les 196 chapeaux restants pour l'article 56854717 et épuisez le stock à 0.
>>> pour _ dans intervalle(196):
...     acheter un article(r, "chapeau: 56854717")
>>> r.hmget("chapeau: 56854717", "quantité", "npurchased")
[b'0', b'200']

Maintenant, quand un utilisateur pauvre est en retard au jeu, il devrait être rencontré un OutOfStockError cela indique à notre application de rendre une page de message d'erreur sur le frontend:

>>>

>>> acheter un article(r, "hat:56854717")
Traceback (most recent call last):
  Fichier "", line 1, dans 
  
  
  
  Fichier "", line 20, dans 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 expirer 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.

Dans 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 >>> de datetime importation timedelta
    2 
    3 >>> # setex: "SET" with expiration
    4 >>> r.setex(
    5 ...     "runner",
    6 ...     timedelta(minutes=1),
    7 ...     valeur="now you see me, now you don't"
    8 ... )
    9 Vrai

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() reviendra Aucun et .exists() reviendra 0:

>>>

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

>>> r.expirer("runner", timedelta(secondes=3))  # Set new expire window
Vrai
>>> # Pause for a few seconds
>>> r.obtenir("runner")
>>> r.existe("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 Objectif
r.setex(name, time, value) Sets the value of key prénom à valeur that expires in temps seconds, where temps can be represented by an int or a Python timedelta objet
r.psetex(name, time_ms, value) Sets the value of key prénom à valeur that expires in time_ms milliseconds, where time_ms can be represented by an int or a Python timedelta objet
r.expire(name, time) Sets an expire flag on key prénom pour temps seconds, where temps can be represented by an int or a Python timedelta objet
r.expireat(name, when) Sets an expire flag on key prénom, où quand can be represented as an int indicating Unix time or a Python datetime objet
r.persist(name) Removes an expiration on prénom
r.pexpire(name, time) Sets an expire flag on key prénom pour temps milliseconds, and temps can be represented by an int or a Python timedelta objet
r.pexpireat(name, when) Sets an expire flag on key prénom, où quand can be represented as an int representing Unix time in milliseconds (Unix time * 1000) or a Python datetime objet
r.pttl(name) Returns the number of milliseconds until the key prénom va expirer
r.ttl(name) Returns the number of seconds until the key prénom va expirer

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 
    3 importation datetime
    4 importation ipaddress
    5 
    6 importation redis
    7 
    8 # Where we put all the bad egg IP addresses
    9 liste noire = ensemble()
dix MAXVISITS = 15
11 
12 ipwatcher = redis.Redis(db=5)
13 
14 tandis que Vrai:
15     _, addr = ipwatcher.blpop("ips")
16     addr = ipaddress.ip_address(addr.décoder("utf-8"))
17     à présent = datetime.datetime.utcnow()
18     addrts = F"addr:now.minute"
19     n = ipwatcher.incrby(addrts, 1)
20     si n >= MAXVISITS:
21         impression(F"Hat bot detected!:  addr")
22         liste noire.ajouter(addr)
23     autre:
24         impression(F"now:  saw addr")
25     _ = ipwatcher.expirer(addrts, 60)

Let’s walk through a few important concepts.

le ipwatcher acts like a consumer, sitting around and waiting for new IPs to be pushed on the "ips" Redis list. It receives them as octets, 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")
16 addr = ipaddress.ip_address(addr.décoder("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:

17 à présent = datetime.datetime.utcnow()
18 addrts = F"addr:now.minute"
19 n = 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. En utilisant .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 OBTENIR 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:

pour _ dans intervalle(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

À présent, Ctrl+C hors de while True loop and you’ll see that the offending IP has been added to your blacklist:

>>>

>>> liste noire
IPv4Address('104.174.118.18')

Can you find the defect in this detection system? The filter checks the minute as .minute plûtot que le 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 enregistrer 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 enregistrer . 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 enregistrer directives:

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

Un 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:

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()
Vrai
>>> 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 objet. 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 enregistrer configuration option.

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

  1. Explicitly, through the Redis command BGSAVE ou redis-py méthode .bgsave()
  2. Implicitly, through the enregistrer configuration option (which you can also set with .config_set() dans 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 Contexte dans BGSAVE refers to.

There’s also ENREGISTRER (.save() dans 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", tandis que 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 liste 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

Vous pouvez utiliser json.dumps() to serialize the dict into a JSON-formatted string:

>>>

>>> importation json
>>> r.ensemble(484272, json.décharges(restaurant_484272))
Vrai

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

>>>

>>> de pprint importation pprint
>>> pprint(json.charges(r.obtenir(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:

>>>

>>> importation yaml  # python -m pip install PyYAML
>>> yaml.déverser(restaurant_484272)
'address: city: New York, state: NY, street: 11 E 30th St, zip: 10016nname: Ravaghntype: Persiann'

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 section 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:

    1 de collections.abc importation MutableMapping
    2 
    3 def setflat_skeys(
    4     r: redis.Redis,
    5     obj: dict,
    6     préfixe: str,
    7     delim: str = ":",
    8     *,
    9     _autopfix=""
dix ) -> Aucun:
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, octets, flotte, int)
23     pour clé, valeur dans obj.articles():
24         clé = _autopfix + clé
25         si isinstance(valeur, allowed_vtypes):
26             r.ensemble(F"prefixdelimkey", valeur)
27         elif isinstance(valeur, MutableMapping):
28             setflat_skeys(
29                 r, valeur, préfixe, delim, _autopfix=F"keydelim"
30             )
31         autre:
32             élever 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)

>>> pour clé dans triés(r.clés("484272*")):  # Filter to this pattern
...     impression(F"repr(key):35repr(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.obtenir("484272:address:street:line1")
b'11 E 30th St'

The final loop above uses r.keys("484272*"), où "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() plutôt que .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 cryptographie 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:

>>>

>>> importation json
>>> de cryptography.fernet importation Fernet

>>> chiffrer = Fernet(Fernet.generate_key())
>>> Info = 
...     "cardnum": 2211849528391929,
...     "exp": [[[[2020, 9],
...     "cv2": 842,
... 

>>> r.ensemble(
...     "user:1000",
...     chiffrer.Crypter(json.décharges(Info).encoder("utf-8"))
... )

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

>>> chiffrer.décrypter(r.obtenir("user:1000"))
b'"cardnum": 2211849528391929, "exp": [2020, 9], "cv2": 842'

>>> json.charges(chiffrer.décrypter(r.obtenir("user:1000")))
'cardnum': 2211849528391929, 'exp': [2020, 9], 'cv2': 842

Parce que Info contains a value that is a liste, 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 chiffrer objet. 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.

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 >>> importation bz2
    2 
    3 >>> goutte = "i have a lot to talk about" * 10000
    4 >>> len(goutte.encoder("utf-8"))
    5 260000
    6 
    7 >>> # Set the compressed string as value
    8 >>> r.ensemble("msg:500", bz2.compresse(goutte.encoder("utf-8")))
    9 >>> r.obtenir("msg:500")
dix b'BZh91AY&SYxdaMx1eux01x11ox91x80@x002lx87'  # ... [truncated]
11 >>> len(r.obtenir("msg:500"))
12 122
13 >>> 260_000 / 122  # Magnitude of savings
14 2131.1475409836066
15 
16 >>> # Get and decompress the value, then confirm it's equal to the original
17 >>> rblob = bz2.décompresser(r.obtenir("msg:500")).décoder("utf-8")
18 >>> rblob == goutte
19 Vrai

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 protocole 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 "+OKrn", while the integer response 1000 would come back as ":1000rn". This can get more complex with other data types such as RESP arrays.

UNE analyseur 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. (Voir .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 toi même. Juste pip installer it, and this will let redis-py see that it’s available and use its HiredisParser au lieu de 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
essayer:
    importation hiredis
    HIREDIS_AVAILABLE = Vrai
sauf ImportError:
    HIREDIS_AVAILABLE = Faux


# redis/connection.py
si HIREDIS_AVAILABLE:
    DefaultParser = HiredisParser
autre:
    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:

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. Pour Amazon AWS, execute the following from your instance shell:

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

Pour 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:

$ exportation REDIS_ENDPOINT="demo.redis.cache.windows.net"
$ redis-cli -h $REDIS_ENDPOINT -p 6380 -a 

le -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:

>>>

>>> importation os
>>> importation 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(hôte=os.environ[[[["REDIS_ENDPOINT"])

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

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.

Emballer

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 Luda 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.

Livres:

Redis in use:

Other: