Maintenir l’honnêteté des caches Redis dans Node.js : invalidation sans lectures obsolètes
Comparez TTL, suppression à l’écriture, pub/sub et invalidation par clé versionnée pour Redis dans Node.js, observez les risques que chacun de ces mécanismes crée, et créez un enveloppe cache avec fonction de défaillance ouverte.
Sources: Redis Caching in Node.js, Part 2: Invalidation That Doesn’t Lie
Imaginez un utilisateur qui télécharge une nouvelle photo de profil, reçoit une réponse indiquant que l’opération a réussi, recharge la page mais voit toujours l’ancienne photo. Il essaie à nouveau avec le même résultat, et quelques minutes plus tard, la nouvelle photo apparaît d’elle-même. Le rapport de bug qui en résulte est presque impossible à reproduire, car Redis continue de fournir une copie de la ligne que la base de données a déjà remplacée. Une base de données lente au moins fournit la bonne réponse ; un cache rapide qui en donne une fausse est pire que pas de cache du tout. Ce guide se concentre sur le chemin d’écriture d’un cache Redis dans Node.js : quatre méthodes pour invalider les entrées, le moment où chacune justifie son coût, la condition de concurrence que chaque méthode laisse ouverte, un wrapper cacheAside() réutilisable qui combine les protections liées au chemin de lecture avec l’invalidation, ainsi que les erreurs qui maintiennent le plus fréquemment des valeurs obsolètes.
Le côté lecture du cache, c’est-à-dire le chargement hors cache, la protection contre les surcharges et ce qui se passe lorsque une clé fréquemment utilisée arrive à échéance, constitue un sujet distinct ; notre aperçu des principes de base et pièges du cache Redis couvre les bases. Aucune de ces techniques ne permet d’éviter des données obsolètes après une écriture, ce qui est justement abordé dans le reste de cet article.
Quatre stratégies d’invalidation
Les stratégies ci-dessous sont listées approximativement dans l’ordre dans lequel vous devriez les envisager, du moins coûteux au plus complexe.
Stratégie 1 : compter uniquement sur le TTL
Attribuez une date d’échéance à chaque entrée, acceptez que la valeur puisse être vieille de ce nombre de secondes au maximum, et enregistrez ce chiffre à un endroit accessible à l’équipe de développement.
C’est la bonne réponse pour des quantités de données supérieures à ce que la plupart des équipes imaginent. Les textes de catalogue, les hiérarchies de catégories, le texte du profil public et les taux de change horaires n’ont que rarement besoin d’être à jour au milliseconde. Lorsqu’une valeur peut être décalée de cinq minutes, une durée de validité de 300 secondes suffit pour le code :
await redis.set(`product:${id}`, JSON.stringify(product), 'EX', 300);
La photo de profil, c’est une autre histoire. La personne qui a effectué la modification s’attend à la voir immédiatement, et une durée de validité ne peut pas le garantir. Pour les données modifiées par l’utilisateur, il faut invalider la donnée dès que l’écriture a lieu.
Stratégie 2 : supprimer l’entrée lors de l’écriture
Après avoir mis à jour la base de données, supprimez la clé de cache correspondante. La prochaine lecture échouera, chargera la nouvelle ligne et réinitialisera le cache :
async function updateUser(id, patch) {
const updated = await db.users.update(id, patch);
await redis.del(`user:${id}`);
return updated;
}
Deux détails permettent à cette méthode de fonctionner correctement.
Supprimer plutôt que d’écraser. Puisque vous disposez déjà de la valeur mise à jour, l’écrire dans Redis avec SET semble être une optimisation gratuite. Le problème apparaît lorsqu’il y a deux écrivains concurrents : leurs commandes SET peuvent arriver à Redis dans l’ordre inverse de leurs enregistrements dans la base de données, ce qui fait que l’écriture antérieure reste en cache jusqu’à expiration du TTL. Les suppressions ne dépendent pas de l’ordre ; quelle que soit la commande qui arrive en dernier, le résultat est une clé vide.
Écrire dans la base de données avant de supprimer. Inverser l’ordre crée une marge importante :
// Wrong: delete before write
await redis.del(`user:${id}`); // 1. cache is empty
const updated = await db.users.update(id, patch); // 2. write the row
// Between 1 and 2, a reader can miss the cache, read the OLD row,
// and put it back. The cache now holds the old value until its TTL.
Entre la suppression et la mise à jour, tout lecteur qui rate le cache chargera la vieille ligne avant de la réinsérer immédiatement.
Écrire d’abord réduit cette marge sans pour autant l’éliminer complètement. Le calendrier suivant montre comment un lecteur lent peut encore restituer des données obsolètes :
Reader Writer
------ ------
GET user:42 -> miss
SELECT user 42 -> old row
UPDATE user 42 (commit)
DEL user:42
SET user:42 <- old row
Le lecteur a récupéré la ligne avant que l’écriture ne soit confirmée, puis l’a stockée après la suppression effectuée par l’écriture. Cette ancienne valeur reste en mémoire tampon jusqu’à expiration de son TTL. Ce phénomène nécessite un lecteur lent et un écrivain qui intervient précisément au mauvais moment, ce qui le rend rare ; cependant, lorsqu’une clé est lue des milliers de fois par minute, le terme « rare » peut facilement signifier qu’il se produit quotidiennement.
La solution standard consiste en une deuxième suppression, planifiée pour durer un peu plus longtemps que le temps nécessaire à une charge normale :
async function updateUser(id, patch) {
const updated = await db.users.update(id, patch);
const key = `user:${id}`;
await redis.del(key);
// Catches a reader that loaded the old row before the commit.
setTimeout(() => redis.del(key).catch(() => {}), 500).unref();
return updated;
}
La fonction unref() empêche que le timer en attente ne maintienne le processus ouvert lors de la fermeture, et le bloc catch vide garantit qu’une suppression échouée ne provoque jamais de rejet non géré.
Deux autres précautions s’imposent :
- Transactions. Si la mise à jour s’effectue au sein d’une transaction de base de données, effectuez la suppression une fois que l’approbation a réussi, en dehors du callback de la transaction. Supprimer avant l’approbation crée à nouveau le mauvais ordre indiqué ci-dessus.
- Pannes. Si le processus s’arrête entre l’approbation et la suppression, l’invalidation est simplement perdue, et seul le TTL finira par la corriger. Lorsque la perte d’une invalidation est inacceptable, déclenchez plutôt l’invalidation à partir d’un flux de capture des données modifiées, par exemple Debezium sur Postgres ou MySQL, ou les flux de changement sur MongoDB. L’invalidation dépend alors de l’approbation elle-même plutôt que du fait qu’un gestionnaire de requêtes survive suffisamment longtemps.
Stratégie 3 : diffuser les invalidations vers les caches en mémoire locale
Lorsque chaque processus lit directement depuis Redis partagé, une seule commande DEL est perçue par tous en même temps, et le mécanisme pub/sub n’ajoute rien.
Cela devient nécessaire lorsque les processus conservent une petite couche en mémoire devant Redis, telle qu’un Map ou lru-cache, afin d’éviter un aller-retour réseau pour les clés très fréquemment utilisées. Lorsque l’instance A supprime une clé de Redis, la copie locale de l’instance B n’est pas affectée. La publication de cette clé invalidée permet à chaque instance de supprimer sa propre copie :
// Every instance runs this at startup.
// A subscribed connection can't run other commands, hence duplicate().
const sub = redis.duplicate();
await sub.subscribe('cache:invalidate');
sub.on('message', (_channel, key) => localCache.delete(key));
// Whoever writes to the database:
async function updateUser(id, patch) {
const updated = await db.users.update(id, patch);
await redis.del(`user:${id}`);
await redis.publish('cache:invalidate', `user:${id}`);
return updated;
}
Remarquez l’appel à duplicate() : une connexion Redis en mode abonné ne peut pas exécuter de commandes ordinaires, donc l’abonnement nécessite une connexion propre.
Si vous utilisez Redis 6 ou une version ultérieure, examinez son support intégré de mise en cache côté client (CLIENT TRACKING) avant d’implémenter votre propre solution. Le serveur se souvient des clés que chaque connexion a récupérées et notifie cette connexion lorsqu’une d’entre elles est modifiée. Ce support varie selon les bibliothèques de client, il convient donc de vérifier que la vôtre le prend en charge.
Stratégie 4 : clés versionnées
Lorsque servir une valeur ancienne constitue un problème de correction plutôt que purement esthétique, intégrez un numéro de version dans la clé :
async function getOrder(id) {
const version = (await redis.get(`order:${id}:ver`)) ?? '0';
const key = `order:${id}:v${version}`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const order = await db.orders.findById(id);
if (!order) return null;
await redis.set(key, JSON.stringify(order), 'EX', 86400);
return order;
}
async function updateOrder(id, patch) {
const updated = await db.orders.update(id, patch);
await redis.incr(`order:${id}:ver`);
return updated;
}
Les lectures récupèrent la version actuelle depuis une clé de compteur et construisent à partir d’elle la clé contenant les données. Les écritures se limitent à incrémenter le compteur.
Exécutez à nouveau la course de stratégie 2 selon ce schéma. Le lecteur lent récupère la ligne obsolète ; pendant ce temps, l’écriture enregistre les modifications et augmente le compteur ; ensuite, le lecteur met en cache ce qu’il a récupéré. Son écriture est dirigée vers order:42:v0, une clé que aucun lecteur ne consultera à nouveau, de sorte que la copie obsolète reste stockée sur une clé abandonnée jusqu’à son expiration. C’est cette immunité qui justifie la complexité supplémentaire.
Les règles qui garantissent sa sécurité :
- Seuls les écrivains touchent au compteur, et ce uniquement avec
INCR. Si un lecteur initialise ce compteur à 0 par une opération commeSET, cela peut annuler une augmentation effectuée un instant auparavant et faire réapparaître la version précédente.
v0 ou v1 restants deviennent à nouveau visibles.Les inconvénients consistent en deux allers-retours par lecture au lieu d’un, ainsi que dans la consommation de mémoire due aux anciennes versions jusqu’à leur expiration. Ce compromis est justifié pour les commandes, les soldes, les niveaux de stock et tout ce qui concerne la facturation, mais excessif pour la plupart des autres données.
Choix d’une stratégie
Une comparaison concise :
- TTL uniquement : facile à mettre en œuvre ; les données peuvent avoir jusqu’à une durée TTL d’ancienneté ; utilisez-le lorsque certains délais d’obsolescence sont acceptables et que vous pouvez en indiquer la durée.
- Suppression à l’écriture : faible complexité ; les données deviennent immédiatement fraîches, à l’exception d’un cas rare de concurrence qui persiste jusqu’à l’expiration du TTL ; c’est la valeur par défaut pour tout ce que les utilisateurs modifient.
Pour un service typique, les dates d’expiration combinées à la suppression au moment de l’écriture suffisent, en plus d’une suppression supplémentaire différée pour les clés fortement lues que les utilisateurs peuvent modifier. Ajoutez le mécanisme pub/sub le jour où vous introduisez un cache en mémoire, et conservez les clés versionnées pour les rares entités où une réponse erronée a un coût financier.
Un wrapper CacheAside() de référence
C’est lorsque la logique de mise en cache est dispersée dans les gestionnaires de requêtes que ces règles sont oubliées ; il est donc avantageux d’écrire cette logique une seule fois et de l’importer partout. Le module ci-dessous utilise ioredis et combine :
- Appels Redis en mode « fail-open » qui redirigent vers la base de données au lieu de rejeter les requêtes,
- mécanisme « stale-while-revalidate » permettant de servir des valeurs légèrement obsolètes pendant une mise à jour,
- mode d’exécution unique par processus, de sorte que les appels concurrents au sein d’un même processus partagent la même charge,
- verrou inter-processus avec déverrouillage par comparaison et suppression, afin que les instances ne reconstruisent pas toutes la même clé,
- invalidation via pub/sub accompagnée d’une deuxième suppression différée.
// cache.js
const Redis = require('ioredis');
const { randomUUID } = require('node:crypto');
const logger = require('./logger');
// Fail fast, so a dead Redis makes requests slower instead of hanging them.
const redis = new Redis(process.env.REDIS_URL, {
commandTimeout: 250,
maxRetriesPerRequest: 1,
});
// A subscribed connection can't run other commands, so it gets its own,
// with default options so it keeps reconnecting.
const sub = new Redis(process.env.REDIS_URL);
const CHANNEL = 'cache:invalidate';
// Compare-and-delete, as in Part 1: never release a lock you no longer own.
const UNLOCK = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
end
return 0
`;
// ---- Invalidation fan-out to in-process caches ----
const listeners = new Set();
sub.subscribe(CHANNEL).catch(err => logger.error({ err }, 'cache: subscribe failed'));
sub.on('message', (_channel, key) => {
for (const fn of listeners) {
try { fn(key); } catch (err) { logger.warn({ err, key }, 'cache: listener failed'); }
}
});
function onInvalidate(fn) {
listeners.add(fn);
return () => listeners.delete(fn);
}
// ---- Fail-open reads and writes ----
async function readEntry(key) {
const raw = await redis.get(key).catch(() => null);
if (!raw) return null;
try { return JSON.parse(raw); } catch { return null; }
}
async function writeEntry(key, value, { freshSec, staleSec }) {
const now = Date.now();
const entry = {
value,
freshUntil: now + freshSec * 1000,
staleUntil: now + (freshSec + staleSec) * 1000,
};
await redis.set(key, JSON.stringify(entry), 'EX', freshSec + staleSec).catch(() => {});
return value;
}
// ---- Single-flight: per process first, then across processes ----
const inflight = new Map();
function once(slot, fn) {
const existing = inflight.get(slot);
if (existing) return existing;
const p = fn().finally(() => inflight.delete(slot));
inflight.set(slot, p);
return p;
}
async function rebuild(key, loader, opts, { wait }, attempt = 0) {
const lockKey = `lock:${key}`;
const token = randomUUID();
// If Redis is down the SET rejects. Treat that as "got the lock" and just load.
const got = await redis.set(lockKey, token, 'NX', 'EX', opts.lockSec).catch(() => 'OK');
if (got === 'OK') {
try {
// Another instance may have finished a rebuild while we were getting the lock.
const entry = await readEntry(key);
if (entry && Date.now() < entry.freshUntil) return entry.value;
return await writeEntry(key, await loader(), opts);
} finally {
await redis.eval(UNLOCK, 1, lockKey, token).catch(() => {});
}
}
// Someone else holds the lock. A background refresh can leave the work to them.
if (!wait) return undefined;
await new Promise(r => setTimeout(r, 50));
const entry = await readEntry(key);
if (entry && Date.now() < entry.freshUntil) return entry.value;
if (attempt >= 20) return loader(); // about a second of waiting: do it ourselves
return rebuild(key, loader, opts, { wait }, attempt + 1);
}
// ---- Public API ----
async function cacheAside(key, loader, { freshSec = 60, staleSec = 300, lockSec = 5 } = {}) {
const opts = { freshSec, staleSec, lockSec };
const entry = await readEntry(key);
const now = Date.now();
if (entry && now < entry.freshUntil) return entry.value;
if (entry && now < entry.staleUntil) {
// Stale but servable: return it now, refresh once in the background.
once(`refresh:${key}`, () => rebuild(key, loader, opts, { wait: false }))
.catch(err => logger.warn({ err, key }, 'cache: refresh failed'));
return entry.value;
}
// Cold, or past staleUntil: the caller waits for a load.
return once(`load:${key}`, () => rebuild(key, loader, opts, { wait: true }));
}
async function invalidate(key, { redeleteMs = 500 } = {}) {
await redis.del(key).catch(() => {});
await redis.publish(CHANNEL, key).catch(() => {});
// Second delete catches a rebuild that read the old row before the write committed.
setTimeout(() => redis.del(key).catch(() => {}), redeleteMs).unref();
}
module.exports = { cacheAside, invalidate, onInvalidate, redis };
L’utilisation de ce mécanisme depuis du code d’application nécessite un appel sur la voie de lecture et un autre sur la voie d’écriture :
const { cacheAside, invalidate } = require('./cache');
const user = await cacheAside(`user:${id}`, () => db.users.findById(id), {
freshSec: 60,
staleSec: 300,
});
async function updateUser(id, patch) {
const updated = await db.users.update(id, patch);
await invalidate(`user:${id}`);
return updated;
}
Plusieurs choix de conception dans l’encapsulant ne sont pas évidents, il convient donc de le lire attentivement avant de l’adopter.
Fonctions d’exécution séparées pour la mise à jour et la charge
Les mises à jour en arrière-plan et les chargements bloquants sont identifiés par des préfixes différents, refresh: et load:. Lorsqu’une mise à jour en arrière-plan perd la course pour le verrou inter-processus, elle abandonne délibérément en renvoyant undefined, car une autre instance s’occupe de la reconstruction. Si un appelant ayant réellement besoin d’une valeur partagée dans cette case ne reçoit rien non plus.
Deuxième lecture du cache après avoir obtenu le verrou
Dès que rebuild() détient le verrou, il lit l’entrée une fois de plus. Un autre processus aurait pu terminer la reconstruction pendant l’attente, et exécuter le chargeur deux fois gaspillerait une requête.
Attente limitée lorsque le verrou est obtenu
Un appelant qui ne parvient pas à obtenir le verrou effectue des tentatives toutes les 50 ms pendant un maximum de 20 fois, soit environ une seconde, puis exécute lui-même le chargeur. Quelques requêtes dupliquées sont préférables à une file d’appelants qui ne s’épuise jamais si le détenteur du verrou est bloqué.
Achat de verrou en mode échec ouvert
Si Redis n’est pas disponible et que SET NX est rejeté, le bloc .catch convertit ce rejet en 'OK', et chaque appelant passe au chargeur. Cela permet au service de rester opérationnel, mais cela signifie également que la base de données doit absorber toute la charge jusqu’à ce que Redis se remette en service. Vérifiez qu’elle en est capable.
Ajustement des paramètres par défaut
lockSecdoit dépasser la latence p99 du chargeur, afin que le verrou ne expire pas au milieu d’une reconstruction.redeleteMsdoit également dépasser le temps d’exécution du chargeur le plus lent. 500 ms conviennent à une requête indexée simple ; une agrégation dans un tableau de bord qui prend quatre secondes nécessite plus de temps.
Ce qui est délibérément omis
L’expiration précoce probabiliste n’est pas incluse. La fonction Stale-while-revalidate élimine déjà le problème de l’expiration brutale pour les clés consultées fréquemment, et combiner ces deux mécanismes dans un même format d’entrée complique la compréhension du moment où une mise à jour a lieu. Pour les clés dont le coût de reconstruction est fortement variable, une vérification probabiliste peut remplacer la comparaison freshUntil dans la fonction cacheAside().
Où se trouve chaque aspect dans le code
- Mise à jour manquée, chargement et stockage :
cacheAside()etrebuild() - Mécanisme Stale-while-revalidate : les champs
freshUntiletstaleUntilde chaque entrée - Mécanisme d’exécution unique par processus :
once() - Mechanisme de verrouillage inter-processus et déverrouillage sécurisé :
rebuild()ainsi que le script LuaUNLOCK
.catch() pour chaque appel à Redis, ainsi que commandTimeoutinvalidate()invalidate() et onInvalidate()Pièges courants qui entraînent des données obsolètes ou endommagées
Ces éléments sont classés approximativement en fonction de l’ampleur des dommages qu’ils ont tendance à provoquer.
Considérer les erreurs Redis comme fatales
Un simple await sur une requête Redis provoque une erreur dès que Redis n’est pas accessible :
const cached = await redis.get(key); // throws when Redis is unreachable
Le cache est un outil d’optimisation. Une panne ne devrait entraîner que des retards, jamais une série de réponses 500. La solution minimale consiste à transformer les échecs en manques de cache :
const cached = await redis.get(key).catch(() => null);
Avec les paramètres par défaut d’ioredis, le seul .catch ne suffit pas. Lorsque la connexion est interrompue, le client met en mémoire tampon les commandes et continue de les réessayer, par défaut jusqu’à 20 fois par requête, au lieu d’échouer rapidement, ce qui fait que les requêtes restent bloquées en attendant un serveur indisponible. En configurant commandTimeout ainsi qu’une valeur faible pour maxRetriesPerRequest, comme le fait le client du wrapper, les requêtes parviennent plus rapidement au chargeur. Chaque requête subit néanmoins quelques centaines de millisecondes de retard en cas de panne, ce qui est acceptable pour des incidents de courte durée. En cas de panne prolongée, un interrupteur de circuit qui cesse d’appeler Redis après plusieurs erreurs consécutives permet de récupérer ce temps perdu.
Invalidation sans TTL
Les suppressions se perdent : un processus plante après l’enregistrement, une déploiement interrompt une requête en cours, ou une diffusion arrive alors qu’un abonné est en train de redémarrer. Sans TTL, l’un de ces événements laisse une clé obsolète dans Redis indéfiniment. Donnez à chaque clé une date d’expiration, même longue comme un jour. Les suppressions maintiennent les entrées à jour la plupart du temps ; l’expiration constitue le mécanisme de sécurité qui empêche une entrée incorrecte de persister éternellement.
Stocker des valeurs sans les serialiser
Redis stocke des chaînes de caractères, donc le passage d’un objet simple corrompt silencieusement l’entrée :
await redis.set(key, JSON.stringify(value)); // correct
await redis.set(key, value); // an object is stored as "[object Object]"
Serialisez avec JSON.stringify lors de l’écriture et parsez avec JSON.parse lors de la lecture. Enveloppez les valeurs dès le début dans un conteneur tel que { value, freshUntil, staleUntil }, même si seules la value est utilisée au début ; l’ajout ultérieur d’une fonction de mise à jour conditionnelle ne vous oblige pas à gérer deux formats en même temps.
Faites également un choix conscient concernant les résultats null. Le conteneur les stocke, une technique appelée cache négatif, de sorte que les demandes répétées pour une ligne inexistante n’atteignent plus la base de données, ce qui est généralement souhaitable. Lorsqu’une ligne peut exister peu de temps après une absence, par exemple un enregistrement récemment inséré consulté sur une réplique en retard, utilisez des échecs de cache avec une durée de validité plus courte.
Clés illimitées générées à partir des entrées utilisateur
const key = `search:${req.query.q}`;
Chaque chaîne de recherche unique génère sa propre clé, et n’importe quel client peut en créer des millions. Ce qui se passe ensuite est régi par la politique maxmemory-policy. Avec allkeys-lru, l’inondation de clés élimine les entrées véritablement fréquentes, ce qui entraîne une chute drastique du taux de réussite des requêtes. Avec noeviction, utilisé par Redis auto-hébergé sauf configuration contraire, la mémoire s’épuise et les écritures sont refusées avec des erreurs OOM.
Le hachage de la requête ne sert à rien : un hachage limite la longueur de chaque clé, mais pas le nombre total de clés. Il est préférable de normaliser l’entrée (supprimer les espaces, mettre en minuscules, ordonner les paramètres) afin que des recherches équivalentes soient mappées sur une seule clé, de conserver ces entrées pour une durée limitée, et de ne mettre en cache que les recherches fréquentes ou coûteuses. Configurez délibérément maxmemory ainsi qu’une politique d’éviction plutôt que de retenir les valeurs par défaut.
Partage d’une instance Redis entre différents environnements
Si le environnement de préparation et celui de production partagent une instance, les deux environnements lisent et écrivent dans le même user:42. Les instances dédiées constituent la véritable solution ; en solution de repli, attribuez un espace de noms à chaque clé, ce que ioredis peut faire pour vous :
const redis = new Redis(process.env.REDIS_URL, {
keyPrefix: `${process.env.APP_ENV}:`,
});
Basez le préfixe sur une variable spécialement conçue comme APP_ENV, car l’environnement de préparation fonctionne souvent avec NODE_ENV=production également. Notez que keyPrefix ne concerne que les clés, pas les canaux pub/sub, il vous faut donc ajouter vous-même l’environnement au nom du canal d’invalidation.
Mutation d’une valeur en mémoire cache partagée
En raison du principe de vol unique, les appels qui se chevauchent en temps reçoivent tous un même objet partagé plutôt que des copies distinctes :
const a = cacheAside('user:42', load); // both wait on one promise
const b = cacheAside('user:42', load);
(await a).name = 'changed';
console.log((await b).name); // 'changed'
Tout ce qui est récupéré depuis Redis est analysé pour former un nouvel objet par appelant, de sorte que le problème se limite au cas des promesses partagées, c’est pourquoi il apparaît rarement et est difficile à reproduire. Considérez les valeurs mémorisées comme immuables, ou utilisez structuredClone() avant de les modifier. Le gel des valeurs retournées avec Object.freeze en phase de développement fait en sorte que le code en mode strict lance une erreur pour l’opération problématique plutôt que de corrompre silencieusement les données.
Points clés
- Fixez une date d’expiration pour chaque clé : la suppression maintient les données à jour, tandis que l’expiration limite la durée pendant laquelle une erreur peut persister.
- Commencez par le principe du cache-aside associé à la suppression en écriture : enregistrez les données dans la base de données, puis supprimez (sans jamais écraser) la clé en dehors de toute transaction. Planifiez une suppression ultérieure pour les clés fréquemment modifiées par les utilisateurs.
Une dernière précaution : tout cela suppose l’existence d’un seul serveur Redis indépendant. Les déploiements Sentinel et Cluster se répliquent de manière asynchrone, et un basculement en cas de panne peut supprimer les dernières écritures, y compris les clés de verrou sur lesquelles le mécanisme de gestion dépend. Tenez compte de ce facteur avant d’utiliser ces verrous pour autre chose que simplement une coordination basée sur les meilleures pratiques.