Redis-Persistierung: RDB, AOF und wann man Daten verliert
1. Überblick
Redis speichert alle Daten im RAM. Falls der Prozess abstürzt, bleiben die Daten nur erhalten, wenn Redis sie zuvor auf die Festplatte geschrieben hat. Dieser Artikel erklärt die beiden Persistierungsmechanismen RDB (Snapshot) und AOF (Append-Only File), wie Sie diese wählen und konfigurieren. Der Artikel listet auch Situationen auf, in denen Sie Daten verlieren können, selbst wenn Sie Persistierung aktiviert haben. Am Ende gibt es Spring-Boot-Code zur Überprüfung des Persistierungsstatus und einen Test mit Testcontainers.
2. RDB: regelmäßige Snapshots
RDB schreibt den gesamten Datensatz in eine Binärdatei, standardmäßig dump.rdb, zu einem bestimmten Zeitpunkt. Konfiguration in redis.conf:
# Snapshot, wenn nach 3600s mindestens 1 Änderung erfolgt ist,
# nach 300s mindestens 100 Änderungen oder nach 60s mindestens 10000 Änderungen
save 3600 1 300 100 60 10000
dbfilename dump.rdb
dir /data
# Schreibbefehle ablehnen, wenn die letzte BGSAVE fehlgeschlagen ist
stop-writes-on-bgsave-error yes
rdbcompression yes
Wenn eine save-Bedingung erfüllt ist, ruft Redis fork() auf. Der untergeordnete Prozess schreibt den Snapshot in eine temporäre Datei und benennt sie dann in dump.rdb um. Der übergeordnete Prozess bedient weiterhin Clients wie gewohnt. Dank des Copy-on-Write-Mechanismus des Betriebssystems verwenden beide Prozesse gemeinsame Speicherpages. Eine Page wird nur kopiert, wenn der übergeordnete Prozess sie ändert.
Sie können auch manuell einen Snapshot erstellen:
redis-cli BGSAVE
redis-cli LASTSAVE
Background saving started
(integer) 1760169600
BGSAVE läuft im Hintergrund. LASTSAVE gibt den Unix-Zeitstempel des letzten erfolgreichen Snapshots zurück. Vermeiden Sie SAVE in der Produktion: dieser Befehl blockiert den gesamten Server, bis der Schreibvorgang abgeschlossen ist.
Vorteile von RDB:
- Die Datei ist kompakt und eignet sich sehr gut zum Backup oder zum Kopieren in andere Umgebungen.
- Der Neustart ist schnell, da Redis nur eine Binärdatei laden muss.
- Beeinträchtigt den Durchsatz zwischen den Snapshots praktisch nicht.
Nachteile: Alle Änderungen nach dem letzten Snapshot gehen verloren, wenn Redis abstürzt. Mit der obigen Konfiguration verlieren Sie im schlimmsten Fall mehrere Minuten Daten.
Hinweis:
fork()auf großen Datensätzen (Dutzende von GB) kann Redis für einige hundert Millisekunden einfrieren. Wenn Sie während eines Snapshots viel schreiben, kann Copy-on-Write den RAM fast verdoppeln. Reservieren Sie ausreichend Speicher und aktivieren Sievm.overcommit_memory = 1auf Linux.
3. AOF: jeden Schreibbefehl protokollieren
AOF protokolliert jeden Befehl, der Daten ändert (SET, INCR, HSET…), am Ende einer Protokolldatei. Beim Starten führt Redis dieses Protokoll erneut aus, um den Datensatz wiederherzustellen.
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. Die appendfsync-Richtlinie
Schreiben in eine Datei bedeutet nicht, dass die Daten auf der Festplatte sind. Daten können noch im Page-Cache des Kernels verbleiben, bis ein fsync()-Befehl ausgeführt wird. Dies ist der entscheidende Punkt für den Datenverlust:
| Wert | Wann fsync | Max. Datenverlust bei Crash | Leistung |
|---|---|---|---|
always |
Nach jedem Schreibbefehl | Praktisch kein Datenverlust | Am langsamsten |
everysec |
Jede Sekunde, auf Background-Thread | Etwa 1 Sekunde (bis zu 2 Sekunden) | Gut, ist Standard |
no |
Das Betriebssystem entscheidet, normalerweise etwa 30 Sekunden | Einige Dutzend Sekunden | Am schnellsten |
Mit everysec kann der bisherige fsync-Befehl nach 2 Sekunden wegen einer langsamen Festplatte immer noch nicht abgeschlossen sein, und Redis blockiert Schreibbefehle, um sich nicht weiter zu verzögern. Das tatsächliche Datenverlustfenster kann daher 2 Sekunden sein, nicht 1.
Es ist auch wichtig, zwischen zwei Arten von Fehlern zu unterscheiden. Wenn nur der Redis-Prozess abstürzt, bleiben Daten, die write() aufgerufen haben, im Page-Cache und der Kernel schreibt sie später auf die Festplatte. Wenn die gesamte Maschine den Strom verliert oder der Kernel in Panik gerät, gehen Teile, die nicht fsync’ed wurden, verloren.
3.2. AOF Rewrite und Multi-Part AOF
AOF wächst im Laufe der Zeit. Zum Beispiel können eine Million INCR counter-Befehle durch einen einzelnen SET counter 1000000-Befehl ersetzt werden. Redis schreibt AOF automatisch um, wenn die Datei doppelt so groß ist wie der letzte Rewrite (auto-aof-rewrite-percentage 100) und mindestens 64 MB groß ist.
Seit Redis 7 befindet sich AOF im appendonlydir-Verzeichnis und besteht aus mehreren Dateien:
appendonlydir/
├── appendonly.aof.1.base.rdb # Base, RDB-Format dank aof-use-rdb-preamble
├── appendonly.aof.1.incr.aof # Schreibbefehle nach Base
└── appendonly.aof.manifest # Beschreibt, welche Dateien zur aktuellen AOF gehören
Wenn Sie aof-use-rdb-preamble yes aktivieren, wird die Base-Datei im RDB-Format geschrieben, sodass sie schnell geladen wird. Der incr-Teil ist immer noch ein Befehls-Log, daher tritt weniger Datenverlust auf. Dies ist der “Hybrid”-Modus und ist seit Redis 4.0 Standard.
4. Welchen Modus sollte man wählen?
| Anforderung | Empfohlene Konfiguration |
|---|---|
| Reiner Cache, Daten können aus DB rekonstruiert werden | Alles deaktivieren: save "", appendonly no |
| Toleriert mehrere Minuten Datenverlust, kompaktes Backup erforderlich | Nur RDB |
| Sessions, Warenkorb, Rate Limiting müssen dauerhaft sein | AOF everysec + RDB für Backup |
| Kein Befehl darf verloren gehen (selten) | AOF always, sollten aber andere Datenbanken in Betracht ziehen |
Die vernünftige Wahl für die meisten Anwendungen besteht darin, beide zu aktivieren: AOF everysec, um Datenverluste zu reduzieren, und RDB für regelmäßige Backups. Wenn beide aktiviert sind, bevorzugt Redis beim Starten AOF, da AOF vollständiger ist.
Hinweis: Wenn Sie Redis nur als Cache verwenden, deaktivieren Sie Persistierung. Das Aktivieren von RDB für einen Cache kostet nur zusätzliche
fork()-Aufrufe, Festplatten-I/O und RAM, ohne Vorteile zu bringen.
5. Persistierungsstatus von einer Spring-Boot-Anwendung prüfen
Dependency hinzufügen:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Die folgende Klasse liest den Persistierungsstatus, fordert einen Snapshot an und wartet auf Replica-Bestätigung:
@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;
}
}
Erklärung zu jeder Methode:
persistenceInfo()ruftINFO persistenceauf und gibt einPropertiesmit Feldern wieaof_enabledundrdb_last_bgsave_statuszurück.triggerSnapshot()verwendetBGSAVE SCHEDULE. Wenn ein AOF Rewrite läuft, plant Redis einen Snapshot statt einen Fehler zurückzugeben.waitForReplicas()ruftWAITauf und gibt die Anzahl der Replicas zurück, die die vorherigen Schreibbefehle bestätigt haben. Abschnitt 6 erklärt, warum Sie diesen Befehl benötigen.
Ein Auszug aus der Ausgabe von INFO persistence, wenn AOF aktiviert ist:
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
Drei Felder, die Sie in Ihr Monitoring aufnehmen und Warnungen setzen sollten, wenn der Wert nicht ok ist: rdb_last_bgsave_status, aof_last_write_status und aof_last_bgrewrite_status. Das Feld rdb_changes_since_last_save zeigt, wie viele Änderungen Sie verlieren würden, wenn Sie sich nur auf RDB verlassen.
5.1. Test mit 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); // Stellen Sie sicher, dass der neue Zeitstempel sich vom alten unterscheidet
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);
}
}
Der erste Test startet Redis 7.4 mit AOF everysec und bestätigt, dass die Konfiguration wirksam ist. @ServiceConnection lenkt spring.data.redis.host/port automatisch zum Container um, daher müssen Sie keine zusätzlichen Properties deklarieren. Der zweite Test wartet maximal 5 Sekunden darauf, dass sich rdb_last_save_time erhöht, und überprüft dann, dass der Snapshot erfolgreich war.
6. Wann verlieren Sie trotzdem Daten?
Das Aktivieren von Persistierung ist nicht ausreichend. Nachfolgend sind die häufigsten Szenarien in der Produktion aufgeführt.
1. Crash zwischen zwei Snapshots. Wenn Sie nur RDB verwenden, verlieren Sie alles, was nach rdb_last_save_time geschrieben wurde.
2. fsync-Fenster. Mit AOF everysec verlieren Sie etwa 1–2 Sekunden beim Maschinensturz. Mit no kann diese Zahl bis zu mehrere Dutzend Sekunden betragen.
3. Failover mit asynchroner Replikation. Der Master antwortet OK an den Client, bevor die Replica den Befehl empfängt. Wenn der Master abstürzt und Sentinel oder Cluster die Replica zum neuen Master hochstufen, gehen nicht replizierte Befehle verloren, auch wenn der Master AOF always hat. WAIT hilft, dieses Risiko zu reduzieren:
template.opsForValue().set("payment:7788", "CAPTURED");
long acked = inspector.waitForReplicas(1, 100);
if (acked < 1) {
log.warn("payment:7788 wurde von keiner Replica bestätigt");
}
WAIT 1 100 blockiert maximal 100 ms, bis mindestens 1 Replica bestätigt hat. WAIT macht Redis nicht zu einem stark konsistenten System: Die Replica kann einen Befehl erhalten haben, diesen aber möglicherweise noch nicht fsync’ed haben. Betrachten Sie dies als Möglichkeit, die Wahrscheinlichkeit von Datenverlust zu verringern, nicht als Garantie.
4. Master startet mit leerem Datensatz neu. Angenommen, der Master hat Persistierung deaktiviert, wird automatisch neu gestartet und läuft dann wieder mit leeren Daten. Die Replica synchronisiert sich mit dem Master und löscht die eigenen Daten. Wenn der Master keine Persistierung hat, lassen Sie ihn nicht automatisch neu starten.
5. Container mountet kein Volume. Redis schreibt weiterhin dump.rdb und AOF, aber ins Container-Dateisystem. Wenn der Container gelöscht und neu erstellt wird, gehen diese Dateien verloren.
services:
redis:
image: redis:7.4
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
volumes:
redis-data:
Auf Kubernetes ist das äquivalente Szenario die Verwendung von emptyDir statt PersistentVolumeClaim.
6. Festplatte voll oder fork() schlägt fehl. Wenn BGSAVE einen Fehler verursacht und stop-writes-on-bgsave-error yes aktiviert ist, lehnt Redis alle Schreibbefehle mit einem Fehler MISCONF ab. Dies schützt die Daten, aber Ihre Anwendung bricht ab. Viele Leute reagieren darauf, indem sie diese Option deaktivieren, und dann läuft Redis ohne Persistierung weiter.
7. Versehentlich FLUSHALL ausgeführt. Mit AOF ist FLUSHALL nur eine Zeile am Ende der incr-Datei. Wenn AOF noch nicht rewritten wurde, können Sie Redis stoppen, diese Zeile löschen und neu starten. Wenn Rewrite bereits stattgefunden hat, gibt es keine Wiederherstellung. Benennen Sie daher diese Befehle um oder blockieren Sie sie mit ACL in der Produktion.
8. Eviction. Dies ist kein Persistierungsfehler, aber das Ergebnis ist dasselbe. Mit maxmemory-policy allkeys-lru löscht Redis Keys, wenn der RAM voll ist. Wichtige Daten, die nicht verloren gehen können, benötigen noeviction.
Hinweis: Verwenden Sie Redis nicht als einzigen Speicher für Geldtransaktionen oder Bestellungen. Wenn Sie “OK bedeutet: nicht verloren” garantieren müssen, verwenden Sie eine relationale Datenbank oder ein System mit Konsens. Redis sollte als Cache oder Beschleunigungsschicht fungieren.
7. Fazit
RDB bietet kompakte Backup-Dateien und schnelle Starts, kann aber mehrere Minuten Daten verlieren. AOF mit everysec reduziert den Datenverlust auf etwa 1–2 Sekunden, kostet aber zusätzliche I/O. Der Hybrid-Modus (aof-use-rdb-preamble) verbindet die Vorteile beider. Viele reale Risiken liegen jedoch außerhalb dieser beiden Mechanismen: asynchrone Replikation, nicht gemountete Volumes, volle Festplatte, FLUSHALL und Eviction. Überwachen Sie die Felder in INFO persistence, verwenden Sie WAIT für wichtige Schreibbefehle und testen Sie Ihre Konfiguration mit Testcontainers wie in Abschnitt 5.