Transactions Redis : atomicité, commandes et verrouillage optimiste

Fondamentaux du mécanisme transactionnel

Redis propose un mode transactionnel centré sur les commandes MULTI, EXEC, DISCARD, WATCH et UNWATCH. Grâce à ce bloc, un client peut préparer un ensemble de commandes qui seront exécutées de façon isolée et séquentielle : pendant le traitement par EXEC, aucune requête d’un autre client ne s’intercale entre deux commandes de la transaction.

Redis garantit également une forme d’atomicité « tout ou rien » à condition que EXEC soit effectivement atteint. Si le client se déconnecte avant EXEC, la file est simplement abandonnée. Si la déconnexion intervient après EXEC, l’ensemble des commandes est traité.

Une nuance s’impose concernant la persistance AOF. Redis s’efforce d’écrire toute la transaction dans un seul appel système write(2). Si un arrêt brutal survient au milieu de cette écriture, une partie seulement de la transaction peut être enregistrée. Au redémarrage, Redis détecte un AOF tronqué et refuse de démarrer ; l’utilitaire redis-check-aof permet d’élaguer la portion incomplète pour restaurer le serveur.

Les commandes de base

MULTI

Démarre un bloc transactionnel. Toutes les commandes suivantes sont mises en attente dans une file et ne sont pas exécutées immédiatement. La réponse est toujours OK.

EXEC

Exécute atomiquement toutes les commandes présentes dans la file. La réponse est un tableau dont chaque élément correspond au résultat d’une commande, dans l’ordre de soumission. Lorsque WATCH est actif, EXEC retourne (nil) si une clé surveillée a été modifiée.

DISCARD

Vide la file courante et fait sortir la connexion du mode transactionnel. Annule aussi la surveillance des clés initiée par WATCH. Retour : OK.

WATCH

Place une ou plusieurs clés sous surveillance en vue d’un verrouillage otpimiste. Complexité O(1) par clé. Retour : OK.

UNWATCH

Libère toutes les clés précédemment surveillées. Elle est implicitement invoquée par EXEC et DISCARD. Complexité O(1).

Exemple d’exécution atomique

Le scénario suivant incrémente deux compteurs en une seule étape :

MULTI
INCR stats:visites
INCR stats:clics
EXEC

Dans le contexte MULTI, chaque commande reçoit la réponse QUEUED. L’appel à EXEC renvoie un tableau, par exemple [42, 7], qui reflète les valeurs successives des deux compteurs.

Types d’erreurs rencontrées

Deux situations d’erreur se distinguent :

  • Erreurs lors de la mise en file : syntaxe invalide, nombre d’arguments incorrect, ou saturation mémoire liée à maxmemory. Un client peut les repérer avant EXEC en vérifiant que chaque réponse vaut bien QUEUED. Depuis Redis 2.6.5, le serveur mémorise ces erreurs, refuse d’exécuter la transaction et retourne une erreur à EXEC.
  • Erreurs lors de l’exécution : une commande correctement mise en file échoue au moment de EXEC, généralement par incompatibilité de type. Les commandes suivantes continuent d’être exécutées.

Exemple d’échec de mise en file :

MULTI
SET msg "bonjour"
INCRBY msg
EXEC

INCRBY msg manque son argument ; à partir de Redis 2.6.5, EXEC retourne une erreur et la transaction est abandonnée.

Exemple d’échec d’exécution :

MULTI
SET journal "2024-01-15"
LPUSH journal "nouvelle entree"
EXEC

LPUSH échoue car journal est une chaîne, mais la transaction poursuit son exécution et renvoie un tableau mêlant OK et un message d’erreur.

Pourquoi Redis ne propose pas de rollback

Redis ne réalise pas de rollback en cas d’erreur d’exécution. Cette décision repose sur deux principes :

  • Les échecs en cours d’exécution découlent presque toujours d’erreurs de programmation — mauvais type, commande mal formée — plutôt que de conditions imprévisiblse en production.
  • Supprimer le mécanisme de rollback allège le moteur et préserve ses performances.

Par ailleurs, un rollback ne corrigerait pas une faute logique du client : incrémenter une valeur de 2 au lieu de 1 ou cibler une mauvaise clé reste une erreur applicative, indépendante de la transaction.

Abandonner une transaction

La commande DISCARD permet d’annuler une transaction en cours :

MULTI
INCR panier:articles
DISCARD

Aucune commande n’est appliquée et la connexion retrouve son comportement normal.

Verrouillage optimiste avec WATCH

WATCH implémente un mécanisme de type check-and-set. On surveille une clé, on relit sa valeur immédiatement avant la transaction, puis on appelle EXEC. Si un autre client modifie la clé entre WATCH et EXEC, la transaction est annulée.

Par exemple, pour incrémenter un compteur sans utiliser INCR :

WATCH compteur:pages
ancienne = GET compteur:pages
nouvelle = ancienne + 1
MULTI
SET compteur:pages nouvelle
EXEC

Si EXEC retourne (nil), un autre client a modifié compteur:pages entre-temps ; il suffit de recommencer la séquence. Dans les systèmes où chaque client accède à des clés distinctes, les conflits restent rares.

Comportement détaillé de WATCH

Les clés surveillées le restent jusqu’à l’appel de EXEC, DISCARD, UNWATCH ou jusqu’à la fermeture de la connexion. Plusieurs appels à WATCH sont cumulatifs.

La surveillance réagit aux mutations effectuées par d’autres clients, y compris celles qui touchent à la structure ou au TTL de la clé. Si le client ayant lancé WATCH modifie lui-même la clé au sein de la transaction, cela ne provoque pas d’annulation. L’expiration passive d’une clé due à son TTL n’est pas interprétée comme une modification.

Construire une opération atomique : l’exemple ZPOP

Redis ne fournit pas de commande ZPOP native pour retirer l’élément au score le plus faible d’un ensemble ordonné. On peut l’émuler avec WATCH :

WATCH scores:joueurs
candidat = ZRANGE scores:joueurs 0 0
membre = candidat[0]
MULTI
ZREM scores:joueurs membre
EXEC

Si la transaction est invalidée par une modification concurrente, on réitère l’opération. Pour des charges élevées, un script Lua est généralement préférable car il évite les boucles de réessai.

Transactions et scripts Lua

Les scripts exécutés via EVAL ou EVALSHA sont intrinsèquement transactionnels : l’intégralité du script s’exécute de façon atomique, sans interruption par un autre client. Ils couvrent donc la plupart des cas d’usage de MULTI/EXEC/WATCH, souvent de manière plus compacte et plus rapide.

Les transactions conservent néanmoins leur pertinence pour exécuter un lot de commandes de façon atomique et pour gérer les conditions de concrurence grâce à WATCH, sans avoir à écrire de script.

Étiquettes: Redis Redis Transactions WATCH Optimistic Locking Lua scripting

Publié le 3 juillet à 17h21