Formation gratuite Python
- Épisode 71 Compétences sociales: Le manuel de vie du développeur de logiciel
- Épisode # 311 Entrez dans le dossier .git
- 25+ meilleurs thèmes WordPress Elementor de 2021
- Briefing WordPress 5.3 | Apprenti WP
- Notifications Fire Slack basées sur des événements WordPress avec des notifications Slack par dorzki • WPShout
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!
Remarque: Cette section est orientée vers l’installation sur Mac OS X ou Linux. Si vous utilisez Windows, Microsoft fork de Redis peut être installé en tant que service Windows. Il suffit de dire que Redis en tant que programme vit le plus confortablement sur une machine Linux et que l’installation et l’utilisation sous Windows risquent d’être difficiles.
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:
-
La première
faire
La commande compile et lie le code source. -
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 dansCHEMIN
).
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 fairesudo 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.
Note de sécurité: Il y a quelques années, l'auteur de Redis avait signalé des vulnérabilités de sécurité dans les versions précédentes de Redis si aucune configuration n'était définie. Redis 3.2 (version actuelle 5.0.3 à compter de mars 2019) a pris des mesures pour empêcher cette intrusion, en définissant le mode protégé
option de Oui
par défaut.
Nous avons explicitement mis lier 127.0.0.1
laisser Redis écouter les connexions uniquement à partir de l'interface localhost, bien que vous deviez étendre cette liste blanche dans un serveur de production réel. Le point de mode protégé
est une sauvegarde qui imitera ce comportement bind-to-localhost si vous ne spécifiez pas autrement quelque chose sous lier
option.
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.
Remarque: Dans le cadre d’une vérification supplémentaire, vous pouvez rechercher l’ID de processus du serveur Redis avec pgrep
:
$ pgrep redis-server
26983
Pour tuer le serveur, utilisez pkill redis-server
à partir de la ligne de commande. Sur Mac OS X, vous pouvez également utiliser arrêt de redis-cli
.
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
, etDEL
, 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
, etensembles
. 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
.
Remarque: Jusqu’à présent, vous avez parlé au serveur Redis par le biais de la messagerie interactive. redis-cli
REPL. Vous pouvez également émettre des commandes directement, de la même manière que vous transmettriez le nom d’un script au python
exécutable, tel que python myscript.py
.
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 queHSET
,HGET
, ouHMSET
. -
Ensembles: Les commandes pour opérer sur les ensembles commencent par un
S
, tel queSCARD
, 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
ouR
. Les exemples comprennentLPOP
etRPUSH
. leL
ouR
se réfère à quel côté de la liste est opéré. Quelques commandes de liste sont également précédées d’unB
, 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.
Remarque: Une caractéristique remarquable du type de liste de Redis est qu’il s’agit d’une liste chaînée plutôt que d’un tableau. Cela signifie que l'ajout est O (1), tandis que l'indexation sur un numéro d'index arbitraire est O (N).
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-server
et 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.
Remarque: La documentation Redis fournit un exemple de faire la même chose avec le redis-cli
, où vous pouvez diriger le contenu d’un fichier local pour effectuer une insertion en masse.
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
Remarque: HINCRBY
fonctionne toujours sur une valeur de hachage qui est une chaîne, mais il essaie d'interpréter la chaîne comme un entier signé en base 10 sur 64 bits pour exécuter l'opération.
Ceci s’applique à d’autres commandes relatives à l’incrémentation et à la décrémentation d’autres structures de données, à savoir: INCR
, INCRBY
, INCRBYFLOAT
, ZINCRBY
, et HINCRBYFLOAT
. Vous obtiendrez une erreur si la chaîne contenant la valeur ne peut pas être représentée sous la forme d'un entier.
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 laracheté
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:
- Explicitly, through the Redis command
BGSAVE
ouredis-py
méthode.bgsave()
- Implicitly, through the
enregistrer
configuration option (which you can also set with.config_set()
dansredis-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:
- Serialize the values into a string with something like
json.dumps()
- 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
.
Remarque: Fernet uses AES 128 encryption in CBC mode. See the cryptographie
docs for an example of using AES 256. Whatever you choose to do, use cryptographie
, 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 >>> 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.
Remarque: 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
ou 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.
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:
[ad_2]