Redis persistence: RDB, AOF et quand perdre des données
1. Aperçu général
Redis conserve toutes les données en RAM. Si le processus meurt, les données ne subsistent que si Redis les a auparavant écrites sur disque. Cet article explique les deux mécanismes de persistence que sont RDB (snapshot) et AOF (append-only file), comment les choisir et les configurer. L’article énumère aussi les situations où vous perdez quand même des données même si la persistence est activée. En fin d’article, il y a du code Spring Boot pour vérifier l’état de la persistence et un test qui s’exécute avec Testcontainers.
2. RDB : faire un snapshot périodiquement
RDB écrit l’ensemble du dataset dans un fichier binaire, par défaut dump.rdb, à un moment donné. Configuration dans redis.conf :
# Snapshot si après 3600s il y a au moins 1 changement,
# après 300s au moins 100 changements, ou après 60s au moins 10000 changements
save 3600 1 300 100 60 10000
dbfilename dump.rdb
dir /data
# Refuser les commandes d'écriture si le dernier BGSAVE a échoué
stop-writes-on-bgsave-error yes
rdbcompression yes
Quand une condition save est satisfaite, Redis appelle fork(). Le processus enfant écrit le snapshot dans un fichier temporaire, puis le renomme en dump.rdb. Le processus père continue de servir les clients normalement. Grâce au mécanisme copy-on-write du système d’exploitation, les deux processus partagent les pages de mémoire. Une page n’est copiée que quand le processus père la modifie.
Vous pouvez aussi faire un snapshot manuel :
redis-cli BGSAVE
redis-cli LASTSAVE
Background saving started
(integer) 1760169600
BGSAVE s’exécute en arrière-plan. LASTSAVE retourne le timestamp Unix du dernier snapshot réussi. Évitez d’utiliser SAVE en production : cette commande bloque l’ensemble du serveur jusqu’à ce que l’écriture soit terminée.
Avantages du RDB :
- Fichier compact, très approprié pour les sauvegardes ou la copie vers un autre environnement.
- Redémarrage rapide car Redis n’a qu’à charger un fichier binaire.
- N’affecte presque pas le débit entre les snapshots.
Inconvénients : tous les changements après le dernier snapshot sont perdus si Redis plante. Avec la configuration ci-dessus, dans le pire cas vous perdez plusieurs minutes de données.
Attention :
fork()sur un dataset volumineux (dizaines de Go) peut faire geler Redis pendant plusieurs centaines de millisecondes. Si vous écrivez beaucoup pendant un snapshot, le copy-on-write peut doubler la RAM utilisée. Laissez de la mémoire libre et activezvm.overcommit_memory = 1sur Linux.
3. AOF : enregistrer chaque commande d’écriture
AOF enregistre chaque commande qui modifie les données (SET, INCR, HSET…) à la fin d’un fichier log. Au démarrage, Redis rejoue ce log pour reconstruire le dataset.
appendonly yes
appenddirname "appendonlydir"
appendfilename "appendonly.aof"
# always | everysec | no
appendfsync everysec
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-use-rdb-preamble yes
3.1. Politique d’appendfsync
Écrire dans un fichier ne signifie pas que les données sont sur le disque. Les données peuvent rester dans le cache des pages du kernel jusqu’à ce qu’un appel à fsync() soit effectué. C’est ici que se décide la quantité de données que vous allez perdre :
| Valeur | Quand fsync | Perte maximale en cas de crash | Performance |
|---|---|---|---|
always |
Après chaque commande d’écriture | Pratiquement aucune perte | Plus lent |
everysec |
Chaque seconde, sur un thread d’arrière-plan | Environ 1 seconde (peut aller jusqu’à 2) | Bon, est la valeur par défaut |
no |
Décidé par l’OS, généralement environ 30 secondes | Plusieurs dizaines de secondes | Plus rapide |
Avec everysec, si le fsync précédent n’est pas terminé après 2 secondes à cause d’un disque lent, Redis va bloquer les commandes d’écriture pour ne pas retarder davantage. Par conséquent, la fenêtre de perte de données réelle peut être de 2 secondes, pas seulement 1.
Il faut aussi distinguer deux types de défaillances. Si seul le processus Redis plante, les données qui ont été write() restent dans le cache des pages et le kernel les écrira sur le disque après. Si la machine perd l’alimentation ou le kernel panic, la partie non fsyncée sera perdue.
3.2. AOF rewrite et AOF multi-fichier
L’AOF grandit avec le temps. Par exemple, 1 million de commandes INCR counter peuvent être remplacées par une seule commande SET counter 1000000. Redis rewrite automatiquement l’AOF quand le fichier est deux fois plus grand que le dernier rewrite (auto-aof-rewrite-percentage 100) et atteint au minimum 64 MB.
À partir de Redis 7, l’AOF se trouve dans le répertoire appendonlydir et se compose de plusieurs fichiers :
appendonlydir/
├── appendonly.aof.1.base.rdb # base, format RDB grâce à aof-use-rdb-preamble
├── appendonly.aof.1.incr.aof # les commandes d'écriture qui se sont produites après la base
└── appendonly.aof.manifest # décrit quels fichiers font partie de l'AOF courant
Quand on active aof-use-rdb-preamble yes, le fichier de base est écrit au format RDB donc se charge rapidement. La partie incr reste un log de commandes donc perd peu de données. C’est le mode « hybrid » et c’est la valeur par défaut depuis Redis 4.0.
4. Quel mode choisir ?
| Besoin | Configuration suggérée |
|---|---|
| Cache pur, données reconstruisibles à partir de la DB | Tout désactiver : save "", appendonly no |
| Accepter de perdre quelques minutes, besoin d’une sauvegarde compacte | RDB uniquement |
| Sessions, panier d’achat, rate limit qui demandent de la durabilité | AOF everysec + RDB pour sauvegarde |
| Ne pouvoir perdre aucune commande (rare) | AOF always, mais considérez l’utilisation d’une autre DB |
Le choix raisonnable pour la plupart des applications est d’activer les deux : AOF everysec pour réduire les pertes, RDB pour avoir un fichier de sauvegarde périodique. Quand les deux sont activés, Redis privilégie le chargement de l’AOF au démarrage car l’AOF est plus complet.
Attention : si vous n’utilisez Redis que comme cache, désactivez la persistence. Activer le RDB pour un cache n’ajoute que
fork(), les I/O disque et la RAM, sans apporter aucun bénéfice.
5. Vérifier la persistence depuis une application Spring Boot
Ajouter la dépendance :
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
La classe suivante lit l’état de la persistence, demande un snapshot et attend la confirmation de la réplica :
@Component
public class PersistenceInspector {
private final StringRedisTemplate template;
public PersistenceInspector(StringRedisTemplate template) {
this.template = template;
}
public Properties persistenceInfo() {
return template.execute((RedisCallback<Properties>) conn ->
conn.serverCommands().info("persistence"));
}
public void triggerSnapshot() {
template.execute((RedisCallback<Object>) conn ->
conn.execute("BGSAVE", "SCHEDULE".getBytes()));
}
public long waitForReplicas(int replicas, long timeoutMs) {
Long acked = template.execute((RedisCallback<Long>) conn ->
(Long) conn.execute("WAIT",
String.valueOf(replicas).getBytes(),
String.valueOf(timeoutMs).getBytes()));
return acked == null ? 0 : acked;
}
}
Explication de chaque méthode :
persistenceInfo()appelleINFO persistenceet retourne unePropertiescontenant des champs commeaof_enabled,rdb_last_bgsave_status.triggerSnapshot()utiliseBGSAVE SCHEDULE. Si un AOF rewrite est en cours, Redis va planifier le snapshot au lieu de retourner une erreur.waitForReplicas()appelleWAITet retourne le nombre de replicas qui ont confirmé les commandes d’écriture précédentes. La section 6 expliquera pourquoi vous avez besoin de cette commande.
Un exemple de sortie de INFO persistence quand l’AOF est activé :
loading:0
rdb_changes_since_last_save:42
rdb_bgsave_in_progress:0
rdb_last_save_time:1760169600
rdb_last_bgsave_status:ok
aof_enabled:1
aof_rewrite_in_progress:0
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
Trois champs que vous devriez inclure dans votre monitoring et sur lesquels mettre des alertes quand la valeur n’est pas ok : rdb_last_bgsave_status, aof_last_write_status et aof_last_bgrewrite_status. Le champ rdb_changes_since_last_save vous indique combien de changements vous allez perdre si vous vous fiez seulement au RDB.
5.1. Test avec Testcontainers
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
@SpringBootTest
@Testcontainers
class RedisPersistenceTest {
@Container
@ServiceConnection(name = "redis")
static GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:7.4"))
.withExposedPorts(6379)
.withCommand("redis-server", "--appendonly", "yes", "--appendfsync", "everysec");
@Autowired
StringRedisTemplate template;
@Autowired
PersistenceInspector inspector;
@Test
void aofShouldBeEnabledWithEverySecPolicy() {
template.opsForValue().set("order:1001", "PAID");
Properties info = inspector.persistenceInfo();
List<String> fsync = template.execute((RedisCallback<List<String>>) conn ->
conn.serverCommands().getConfig("appendfsync")
.values().stream().map(Object::toString).toList());
assertThat(info.getProperty("aof_enabled")).isEqualTo("1");
assertThat(info.getProperty("aof_last_write_status")).isEqualTo("ok");
assertThat(fsync).containsExactly("everysec");
}
@Test
void backgroundSnapshotShouldSucceed() throws InterruptedException {
template.opsForValue().set("customer:42", "Lan");
long before = Long.parseLong(inspector.persistenceInfo().getProperty("rdb_last_save_time"));
Thread.sleep(1100); // s'assurer que le nouveau timestamp est différent de l'ancien
inspector.triggerSnapshot();
Properties info = inspector.persistenceInfo();
for (int i = 0; i < 50 && Long.parseLong(info.getProperty("rdb_last_save_time")) <= before; i++) {
Thread.sleep(100);
info = inspector.persistenceInfo();
}
assertThat(info.getProperty("rdb_last_bgsave_status")).isEqualTo("ok");
assertThat(Long.parseLong(info.getProperty("rdb_last_save_time"))).isGreaterThan(before);
}
}
Le premier test démarre Redis 7.4 avec AOF everysec et confirme que la configuration est bien en vigueur. @ServiceConnection oriente automatiquement spring.data.redis.host/port vers le container, vous n’avez donc besoin de déclarer aucune property supplémentaire. Le deuxième test attend jusqu’à 5 secondes que rdb_last_save_time augmente, puis vérifie que le snapshot a réussi.
6. Quand perdez-vous quand même des données ?
Activer la persistence ne suffit pas. Voici les situations les plus courantes en production.
1. Crash entre deux snapshots. Si vous n’utilisez que le RDB, vous perdez tout ce qui a été écrit après rdb_last_save_time.
2. Fenêtre de fsync. Avec AOF everysec, vous perdez environ 1–2 secondes si la machine s’effondre. Avec no, ce chiffre peut atteindre plusieurs dizaines de secondes.
3. Basculement avec réplication asynchrone. Le master retourne OK au client avant que la replica ne reçoive la commande. Si le master meurt et que Sentinel ou Cluster élève la replica au rang de master, les commandes qui n’ont pas eu le temps d’être répliquées disparaîtront, même si le master avait AOF always. WAIT aide à réduire ce risque :
template.opsForValue().set("payment:7788", "CAPTURED");
long acked = inspector.waitForReplicas(1, 100);
if (acked < 1) {
log.warn("payment:7788 n'a pas été confirmé par la replica");
}
WAIT 1 100 bloque jusqu’à 100 ms pour que au moins 1 replica confirme. WAIT ne rend pas Redis fortement cohérent : la replica peut recevoir la commande mais ne pas avoir fsyncée. Considérez cela comme un moyen de réduire la probabilité de perte de données, pas comme une garantie.
4. Le master redémarre avec un dataset vide. Supposez que le master ait la persistence désactivée, soit redémarré automatiquement et revienne avec des données vides. La replica se synchronisera avec le master et supprimera ses propres données. Si le master n’a pas la persistence activée, ne le laissez pas redémarrer automatiquement.
5. Container sans volume monté. Redis écrit toujours dump.rdb et l’AOF, mais dans le filesystem du container. Quand le container est supprimé et créé à nouveau, ces fichiers disparaissent.
services:
redis:
image: redis:7.4
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
volumes:
redis-data:
Sur Kubernetes, la situation équivalente est d’utiliser emptyDir au lieu de PersistentVolumeClaim.
6. Disque plein ou fork() qui échoue. Quand BGSAVE échoue et que stop-writes-on-bgsave-error yes est activé, Redis refuse toute commande d’écriture avec une erreur MISCONF. Ce comportement protège les données mais rend votre application inutilisable. Beaucoup de gens réagissent en désactivant cette option, et Redis continue sans persistence.
7. Exécuter accidentellement FLUSHALL. Avec l’AOF, la commande FLUSHALL n’est qu’une seule ligne à la fin du fichier incr. Si l’AOF n’a pas encore été rewrité, vous pouvez arrêter Redis, supprimer cette ligne et redémarrer. Si le rewrite s’est déjà produit, il n’y a plus de moyen de récupérer. C’est pourquoi vous devriez renommer ou bloquer cette commande avec l’ACL en production.
8. Éviction. Ce n’est pas une erreur de persistence, mais le résultat est identique. Avec maxmemory-policy allkeys-lru, Redis supprimera les clés quand la RAM est pleine. Les données qui ne peuvent pas être perdues ont besoin de noeviction.
Attention : n’utilisez pas Redis comme unique lieu de stockage pour les données d’argent ou les commandes. Si vous avez besoin de la garantie « j’ai retourné OK donc vous ne pouvez pas perdre », utilisez une base de données relationnelle ou un système à consensus. Redis devrait jouer le rôle de cache ou d’accélérateur.
7. Conclusion
Le RDB donne un fichier de sauvegarde compact et un démarrage rapide, mais peut perdre plusieurs minutes de données. L’AOF avec everysec réduit la perte à environ 1–2 secondes, au prix d’une I/O supplémentaire. Le mode hybrid (aof-use-rdb-preamble) combine les avantages des deux. La plupart des risques réels se situent en dehors de ces deux mécanismes : réplication asynchrone, volumes non montés, disque plein, FLUSHALL et éviction. Surveillez les champs de INFO persistence, utilisez WAIT pour les commandes d’écriture importantes, et testez votre configuration avec Testcontainers comme dans la section 5.