Redis Best Practices, Partie 2

La deuxième partie du cycle de traduction de Redis Best Practices de Redis Labs, et il discute des modèles d'interaction et des modèles de stockage de données.

La première partie est ici .

Modèles d'interaction


Redis peut fonctionner non seulement comme un SGBD traditionnel, mais aussi ses structures et commandes peuvent être utilisées pour échanger des messages entre microservices ou processus. L'utilisation généralisée des clients Redis, la vitesse et l'efficacité du serveur et du protocole, ainsi que les structures classiques intégrées vous permettent de créer vos propres workflows et mécanismes d'événement. Dans ce chapitre, nous couvrirons les sujets suivants:

  • file d'attente d'événements;
  • blocage avec Redlock;
  • Pub / Sub;
  • événements distribués.

File d'attente des événements


Les listes dans Redis sont des listes de lignes ordonnées, très similaires aux listes liées que vous connaissez peut-être. L'ajout d'une valeur à une liste (push) et la suppression d'une valeur d'une liste (pop) sont des opérations très légères. Comme vous pouvez l'imaginer, c'est une très bonne structure pour gérer une file d'attente: ajoutez des éléments au début et lisez-les depuis la fin (FIFO). Redis fournit également des fonctionnalités supplémentaires qui rendent ce modèle plus efficace, fiable et facile à utiliser.

Les listes contiennent un sous-ensemble de commandes qui vous permettent d'exécuter un comportement de "blocage". Le terme «blocage» fait référence à une connexion avec un seul client. En fait, ces commandes ne permettent pas au client de faire quoi que ce soit tant qu'une valeur n'apparaît pas dans la liste ou jusqu'à l'expiration du délai. Cela élimine le besoin d'interroger Redis, en attendant le résultat. Puisque le client ne peut rien faire pendant qu'il attend une valeur, nous aurons besoin de deux clients ouverts pour illustrer ceci:
#Client 1Client 2
1
> BRPOP my-q 0
[valeur attendue]
2
> LPUSH my-q hello
(integer) 1
1) "my-q"
2) "hello"
[client déverrouillé, prêt à accepter les commandes]
3
> BRPOP my-q 0
[valeur attendue]

Dans cet exemple, à l'étape 1, nous voyons que le client bloqué ne renvoie rien immédiatement, car il ne contient rien. Le dernier argument est le temps d'attente. Ici, 0 signifie attente éternelle. Sur la deuxième ligne , une valeur est entrée dans my-q et le premier client quitte immédiatement l'état de blocage. Sur la troisième ligne, BRPOP est appelé à nouveau (vous pouvez le faire en boucle dans l'application) et le client attend également la valeur suivante. En appuyant sur «Ctrl + C», vous pouvez briser le verrou et quitter le client.

Inversons l'exemple et voyons comment BRPOP fonctionne avec une liste non vide:
#Client 1Client 2
1
> LPUSH my-q hello
(integer) 1
2
> LPUSH my-q hej
(integer) 2
3
> LPUSH my-q bonjour
(integer) 3
4
> BRPOP my-q 0
1) "my-q"
2) "hello"
5
> BRPOP my-q 0
1) "my-q"
2) "hej"
6
> BRPOP my-q 0
1) "my-q"
2) "bonjour"
7
> BRPOP my-q 0
[valeur attendue]

Dans les étapes 1 à 3, nous ajoutons 3 valeurs à la liste et constatons que la réponse augmente, indiquant le nombre d'éléments dans la liste. L'étape 4, malgré l'appel de BRPOP, renvoie immédiatement la valeur. Cela est dû au fait que le comportement de blocage se produit uniquement lorsqu'il n'y a aucune valeur dans la file d'attente. Nous pouvons voir la même réponse instantanée aux étapes 5 à 6, car cela se fait pour chaque élément de la file d'attente. À l'étape 7, BRPOP ne trouve rien dans la file d'attente et bloque le client jusqu'à ce que quelque chose soit ajouté.

Souvent, les files d'attente représentent un travail qui doit être effectué dans un autre processus (travailleur). Dans ce type de charge de travail, il est important que le travail ne disparaisse pas si le travailleur tombe pour une raison quelconque pendant l'exécution. Redis prend en charge ce type de file d'attente. Pour ce faire, utilisez la commande BRPOPLPUSH au lieu de BRPOP. Elle attend une valeur dans une liste, et dès qu'elle y apparaît, la met dans une autre liste. Cela se fait de manière atomique, il est donc impossible pour deux travailleurs de modifier la même valeur. Voyons voir comment ça fonctionne:
#Client 1Client 2
1
> LINDEX worker-q 0
(nil)
2[Si le résultat n'est pas nul, traitez-le d'une manière ou d'une autre et passez à l'étape 4]
3
> LREM worker-q -1 [   1]
(integer) 1
[retour à l'étape 1]
4
> BRPOPLPUSH my-q worker-q 0
[valeur attendue]
5
> LPUSH my-q hello
"hello"
[client déverrouillé, prêt à accepter les commandes]
6[bonjour]
7
> LREM worker-q -1 hello
(integer) 1
8[retour à l'étape 1]

Aux étapes 1 à 2, nous ne faisons rien, car travailleur-q est vide. Si quelque chose est revenu, nous le traitons et le supprimons, puis revenons à l'étape 1 pour vérifier si quelque chose est entré dans la file d'attente. Ainsi, nous effaçons d'abord la file d'attente du travailleur et effectuons le travail existant. À l'étape 4, nous attendons que la valeur apparaisse dans my-q , et lorsqu'elle le fait, elle est transférée atomiquement vers worker-q . Ensuite, nous traitons en quelque sorte «bonjour» , après quoi nous le supprimons de worker-q et revenons à l'étape 1. Si le processus meurt à l'étape 6, la valeur reste toujours dans worker-q . Après avoir redémarré le processus, nous supprimerons immédiatement tout ce qui n'a pas été supprimé à l'étape 7.

Ce modèle réduit considérablement la probabilité de perte d'emploi, mais uniquement si le travailleur décède entre les étapes 2 et 3 ou 5 et 6, ce qui est peu probable, mais les meilleures pratiques en tiendront compte dans la logique du travailleur.

Serrure avec redlock


Parfois, dans le système, il est nécessaire de bloquer certaines ressources. Cela peut être nécessaire pour appliquer des changements importants qui ne peuvent pas être résolus dans un environnement concurrentiel. Objectifs de blocage:

  • permettre à un et un seul travailleur de capturer la ressource;
  • être capable de libérer de manière fiable l'objet de verrouillage;
  • Ne verrouillez pas la ressource hermétiquement (elle doit être déverrouillée après un certain temps).

Redis est une bonne option pour implémenter le blocage, car il a un modèle de données basé sur des clés simples, et chaque fragment est monothread et assez rapide. Il existe une excellente implémentation de verrouillage utilisant Redis appelé Redlock.
Les clients Redlock sont disponibles pour presque toutes les langues, cependant, il est important de savoir comment Redlock fonctionne afin de l'utiliser de manière sûre et efficace.

Tout d'abord, vous devez comprendre que Redlock est conçu pour fonctionner sur au moins 3 machines avec des instances Redis indépendantes. Cela élimine le seul point de défaillance de votre mécanisme de verrouillage, ce qui peut entraîner un blocage de toutes les ressources. Un autre point à comprendre est que même si les horloges des machines ne doivent pas être synchronisées à 100%, elles doivent fonctionner de la même manière - le temps se déplace à la même vitesse: une seconde sur la machine et la même qu'une seconde sur la machine B.

La définition d'un objet de verrouillage avec Redlock commence par l'obtention d'un horodatage avec une précision en millisecondes. Vous devez également indiquer à l'avance l'heure de blocage. Ensuite, l'objet de blocage est défini en définissant (SET) la clé avec une valeur aléatoire (uniquement si cette clé n'existe pas encore) et en définissant le délai d'expiration de la clé. Ceci est répété pour chaque instance indépendante. Si l'instance tombe, elle est immédiatement ignorée. Si l'objet verrou a été installé avec succès sur la plupart des instances avant l'expiration du délai, il est alors considéré comme capturé. Le temps d'installation ou de mise à jour de l'objet verrou est le temps nécessaire pour atteindre l'état de verrouillage, moins le temps de verrouillage prédéfini. En cas d'erreur ou de timeout, déverrouillez toutes les instances et réessayez.

Pour libérer l'objet de verrouillage, il est préférable d'utiliser un script Lua qui vérifiera si la valeur aléatoire attendue se trouve dans le jeu de clés. Si tel est le cas, vous pouvez le supprimer, sinon il est préférable de laisser les clés, car il peut s'agir d'objets de verrouillage plus récents.

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

Le processus Redlock offre de bonnes garanties et l'absence d'un point de défaillance unique, vous pouvez donc être complètement sûr que les objets de verrouillage unique seront distribués et qu'aucun verrouillage mutuel ne se produira.

Pub / Sub


En plus du stockage de données, Redis peut également être utilisé comme plateforme Pub / Sub (éditeur / abonné). Dans ce modèle, un éditeur peut envoyer des messages à n'importe quel nombre d'abonnés à la chaîne. Ce sont des messages basés sur le principe «shot and oublie», c'est-à-dire que si le message est libéré et que l'abonné n'existe pas, alors le message disparaît sans possibilité de récupération.
En s'abonnant au canal, le client passe en mode abonné et ne peut plus appeler de commandes - il devient en lecture seule. L'éditeur n'a pas de telles restrictions.

Vous pouvez vous abonner à plusieurs chaînes. Nous commençons par nous abonner aux deux chaînes météo et sportives à l'aide de la commande SUBSCRIBE:

> SUBSCRIBE weather sports
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "weather"
3) (integer) 1
1) "subscribe"
2) "sports"
3) (integer) 2

Dans un client séparé (une autre fenêtre de terminal, par exemple), nous pouvons publier des messages sur n'importe lequel de ces canaux à l'aide de la commande PUBLIER:

> PUBLISH sports oilers/7:leafs/1
(integer) 1

Le premier argument est le nom du canal, le second est le message. Le message peut être quelconque, dans ce cas, il s'agit d'un compte codé dans le jeu. La commande renvoie le nombre de clients auxquels le message sera remis. Dans le client abonné, nous voyons immédiatement le message:

1) "message"
2) "sports"
3) "oilers/7:leafs/1"

La réponse contient trois éléments: une indication qu'il s'agit d'un message, d'un canal d'abonnement et, en fait, d'un message. Le client immédiatement après avoir reçu revient à écouter la chaîne.

De retour à l'éditeur, nous pouvons publier un autre message:

> PUBLISH weather snow/-4c
(integer) 1

Chez l'abonné, nous verrons le même format, mais avec un canal différent avec le message:

1) "message"
2) "weather"
3) "snow/-4c"

Posons un message sur une chaîne où il n'y a pas d'abonnés:

> PUBLISH currency CADUSD/0.787
(integer) 0

Étant donné que personne n'écoute le canal monétaire , la réponse sera 0. Ce message a disparu et les clients qui, après s'être abonnés à ce canal, ne recevront pas de notification concernant ce message - il a été envoyé et oublié.

En plus de s'abonner à une seule chaîne, Redis permet de s'abonner à des chaînes par masque. Le masque de style glob est transmis à la commande PSUBSCRIBE:

> PSUBSCRIBE sports:*

Le client recevra des messages de tous les canaux, en commençant par le sport: . Dans un autre client, appelez les commandes suivantes:

> PUBLISH sports:hockey oilers/7:leafs/1
(integer) 1
> PUBLISH sports:basketball raptors/33:pacers/7
(integer) 1
> PUBLISH weather:edmonton snow/-4c
(integer) 0

Veuillez noter que les deux premières équipes renvoient 1, tandis que la dernière renvoie 0. Et bien que nous ne soyons pas directement abonnés aux sports: hockey ou sports: basket , le client reçoit des messages via un abonnement par masque. Dans la fenêtre client-abonné, nous pouvons voir qu'il n'y a de résultats que pour les canaux correspondant au masque.

1) "pmessage"
2) "sports:*"
3) "sports:hockey"
4) "oilers/7:leafs/1"
1) "pmessage"
2) "sports:*"
3) "sports:basketball"
4) "raptors/33:pacers/7"

Cette sortie est légèrement différente de la sortie de la commande SUBSCRIBE car elle contient le masque lui-même, ainsi que le vrai nom du canal.

Événements distribués


Le schéma de messagerie Pub / Sub de Redis peut être étendu pour créer des événements distribués intéressants. Disons que nous avons une structure qui est stockée dans une table de hachage, mais nous voulons mettre à jour les clients uniquement lorsqu'un seul champ dépasse la valeur numérique définie par l'abonné. Nous allons écouter les canaux par masque et extraire le hachage en statut . Dans cet exemple, nous nous intéressons à update_status avec les valeurs 5-9.

> PSUBSCRIBE update_status:[5-9]
1) "psubscribe"
2) "update_status:[5-9]"
3) (integer) 1
...

Pour changer la valeur status / error_level , nous avons besoin de deux commandes qui peuvent être exécutées séquentiellement ou dans le bloc MULTI / EXEC. La première commande définit le niveau et la seconde publie une notification avec la valeur encodée dans le canal lui-même.

> HSET status error_level 5
(integer) 1
> PUBLISH update_status:5 0
(integer) 1

Dans la première fenêtre, nous voyons que le message a été reçu, et après cela, vous pouvez basculer vers un autre client et appeler la commande HGETALL:

...
1) "pmessage"
2) "update_status:[5-9]"
3) "update_status:5"
4) "0"

> HGETALL status
1) "error_level"
2) "5"

Nous pouvons également utiliser cette méthode pour mettre à jour la variable locale d'un processus long. Cela peut permettre à plusieurs instances du même processus d'échanger des données en temps réel.

Pourquoi ce modèle est-il meilleur que d'utiliser Pub / Sub? Lorsque le processus redémarre, il peut simplement obtenir l'état entier et commencer à écouter. Les modifications seront synchronisées entre un nombre quelconque de processus.

Modèles de stockage des données


Il existe plusieurs modèles pour stocker des données structurées dans Redis. Dans ce chapitre, nous considérerons les éléments suivants:

  • stockage de données dans JSON;
  • installations de stockage.

Stockage de données JSON


Il existe plusieurs options pour stocker des données JSON dans Redis. La forme la plus courante consiste à sérialiser l'objet à l'avance et à l'enregistrer sous une clé spéciale:

> SET car "{\"colour\":\"blue\",\"make\":\"saab\",\"model\":93,\"features\":[\"powerlocks\",\"moonroof\"]}"
OK
> GET car
"{\"colour\":\"blue\",\"make\":\"saab\",\"model\":93,\"features\":[\"powerlocks\",\"moonroof\"]}"

Cela semble simple, mais il présente de très graves inconvénients:

  • la sérialisation nécessite des ressources informatiques clientes pour lire et écrire;
  • Le format JSON augmente la taille des données;
  • Redis ne dispose que d'une manière indirecte de gérer les données en JSON.

Le premier couple de points peut être négligeable sur de petites quantités de données, mais les coûts augmenteront à mesure que les données augmentent. Cependant, le troisième point est le plus critique.

Avant Redis 4.0, la seule façon de travailler avec JSON dans Redis était d'utiliser un script Lua dans le module cjson. Cela a partiellement résolu le problème, même si cela restait un goulot d'étranglement et créait des tracas supplémentaires avec l'apprentissage de Lua. En outre, de nombreuses applications ont simplement reçu la chaîne JSON entière, l'ont désérialisée, ont travaillé avec les données, les ont sérialisées et les ont à nouveau enregistrées. Ceci est un contre-modèle. Il y a un grand risque de perdre des données de cette manière.

#Instance d'application # 1Instance d'application # 2
1
> GET my-car
2[désérialiser, changer la couleur de la machine et sérialiser à nouveau]
> GET my-car
3
> SET my-car

[nouvelle valeur de l'instance # 1]
[désérialiser, changer de modèle de machine et sérialiser à nouveau]
4
> SET my-car

[nouvelle valeur de l'instance # 2]
5
> GET my-car

Le résultat sur la ligne 5 montrera les changements uniquement à l'instance 2, et le changement de couleur par l'instance 1 sera perdu.

Redis version 4.0 et supérieure a la possibilité d'utiliser des modules. ReJSON est un module qui fournit un type de données spécial et des commandes pour une interaction directe avec lui. ReJSON enregistre les données au format binaire, ce qui réduit la taille des données stockées, fournit un accès plus rapide aux éléments sans passer de temps sur la dé / sérialisation.

Pour utiliser ReJSON, vous devez l'installer sur un serveur Redis ou l'activer dans Redis Enterprise.

L'exemple précédent utilisant ReJSON ressemblerait à ceci:

#Instance d'application # 1Instance d'application # 2
1
> JSON.SET car2 . '{"colour": "blue",  "make":"saab", "model":93,  "features": ["powerlocks",  "moonroof"]}‘
OK
2
> JSON.SET car2 colour '"red"'
OK
3
> JSON.SET car2 model '95'
OK
> JSON.GET car2 .
"{\"colour\":\"red",\"make\":\"saab\",\"model\":95,\"features\":[\"powerlocks\",\"moonroof\"]}"

ReJSON fournit un moyen plus sûr, plus rapide et plus intuitif de travailler avec les données JSON dans Redis, en particulier dans les cas où des modifications atomiques des éléments imbriqués sont nécessaires.

Stockage d'objets


À première vue, le type de données Redis «table de hachage» standard peut sembler très similaire à un objet JSON ou à un autre type. Il est beaucoup plus facile de créer des champs sous forme de chaîne ou de nombre et d'éviter les structures imbriquées. Cependant, après avoir calculé le «chemin» de chaque champ, vous pouvez «aplatir» l'objet et l'enregistrer dans la table de hachage Redis.

{
    "colour": "blue",
    "make": "saab",
    "model": {
        "trim": "aero",
        "name": 93
    },
    "features": ["powerlocks", "moonroof"]
}

En utilisant JSONPath (XPath pour JSON), nous pouvons représenter chaque élément au même niveau de la table de hachage:

> HSET car3 colour blue
> HSET car3 make saab
> HSET car3 model.trim aero
> HSET car3 model.name 93
> HSET car3 features[0] powerlocks
> HSET car3 features[1] moonroof

Pour plus de clarté, les commandes sont répertoriées séparément, mais de nombreux paramètres peuvent être transmis à HSET.

Vous pouvez maintenant demander l'intégralité de l'objet ou son champ individuel:

> HGETALL car3
 1) "colour"
 2) "blue"
 3) "make"
 4) "saab"
 5) "model.trim"
 6) "aero"
 7) "model.name"
 8) "93"
 9) "features[0]"
10) "powerlocks"
11) "features[1]"
12) "moonroof"

> HGET car3 model.trim
"aero"

Bien que cela fournisse un moyen rapide et utile de récupérer un objet stocké dans Redis, il présente ses inconvénients:

  • dans différentes langues et bibliothèques, l'implémentation de JSONPath peut être différente, provoquant une incompatibilité. Dans ce cas, il vaut la peine de sérialiser et de désérialiser les données avec un seul outil;
  • prise en charge des baies:
    • les tableaux clairsemés peuvent être problématiques;
    • il est impossible d'effectuer de nombreuses opérations, telles que l'insertion d'un élément au milieu d'un tableau.

  • Consommation inutile des ressources dans les clés JSONPath.

Ce modèle est à peu près le même que ReJSON. Si ReJSON est disponible, dans la plupart des cas, il est préférable de l'utiliser. Cependant, le stockage d'objets de la manière ci-dessus présente un avantage sur ReJSON: l'intégration avec l'équipe Redis SORT. Cependant, cette commande est complexe sur le plan informatique et est un sujet complexe distinct au-delà de la portée de ce modèle.

La prochaine partie de conclusion couvrira les modèles de séries chronologiques, les modèles de limite de vitesse, les modèles de filtre Bloom, les compteurs et l'utilisation de Lua dans Redis.

PS J'ai essayé d'adapter le plus possible le texte de ces articles en anglais «barbare» en russe, mais si vous pensez que quelque part l'idée est incompréhensible ou incorrecte, corrigez-moi dans les commentaires.

Source: https://habr.com/ru/post/undefined/


All Articles