Persistência no Redis: RDB, AOF e quando você perde dados
1. Visão geral
O Redis mantém todos os dados em RAM. Se o processo morrer, os dados sobrevivem apenas se o Redis já tiver gravado para o disco anteriormente. Este artigo explica os dois mecanismos de persistência: RDB (snapshot) e AOF (arquivo somente-append), como escolher e configurar cada um. O artigo também lista situações onde você ainda perde dados mesmo com persistência ativada. No final, há código Spring Boot para verificar o estado da persistência e um teste rodando com Testcontainers.
2. RDB: tirando snapshots periodicamente
RDB grava todo o dataset em um arquivo binário, por padrão dump.rdb, em um determinado momento. Configure em redis.conf:
# Snapshot se após 3600s houver pelo menos 1 alteração,
# após 300s houver pelo menos 100 alterações, ou após 60s houver pelo menos 10000 alterações
save 3600 1 300 100 60 10000
dbfilename dump.rdb
dir /data
# Rejeita comandos de escrita se o último BGSAVE falhou
stop-writes-on-bgsave-error yes
rdbcompression yes
Quando uma condição save é atendida, o Redis chama fork(). O processo filho grava o snapshot em um arquivo temporário, depois renomeia para dump.rdb. O processo pai continua servindo clientes normalmente. Graças ao mecanismo copy-on-write do sistema operacional, ambos os processos compartilham páginas de memória. Uma página só é copiada quando o processo pai a modifica.
Você também pode tirar um snapshot manualmente:
redis-cli BGSAVE
redis-cli LASTSAVE
Background saving started
(integer) 1760169600
BGSAVE funciona em background. LASTSAVE retorna o timestamp Unix do snapshot bem-sucedido mais recente. Evite usar SAVE em produção: esse comando bloqueia todo o servidor até terminar de gravar.
Vantagens do RDB:
- Arquivo compacto, ótimo para backup ou cópia para outro ambiente.
- Reinicialização rápida, pois o Redis só precisa carregar um arquivo binário.
- Praticamente não afeta o throughput entre snapshots.
Desvantagens: todas as alterações após o último snapshot serão perdidas se o Redis travar. Com a configuração acima, no pior caso você perde vários minutos de dados.
Nota:
fork()em datasets grandes (dezenas de GB) pode fazer o Redis congelar por centenas de milissegundos. Se você escrever muito durante um snapshot, copy-on-write pode fazer a RAM quase dobrar. Reserve memória extra e ativevm.overcommit_memory = 1no Linux.
3. AOF: registrando cada comando de escrita
AOF grava cada comando que modifica dados (SET, INCR, HSET…) no final de um arquivo de log. Ao iniciar, o Redis executa novamente esse log para reconstruir o 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. Estratégia de appendfsync
Gravar em arquivo não significa que os dados já estão no disco. Os dados podem ficar em cache no kernel até que um comando fsync() seja executado. Este é o ponto crucial que determina quanto dados você perde:
| Valor | Quando fsync | Perda máxima ao travar | Performance |
|---|---|---|---|
always |
Após cada comando de escrita | Praticamente nenhuma | Mais lento |
everysec |
A cada segundo, em thread de background | Cerca de 1 segundo (pode chegar a 2) | Bom, é o padrão |
no |
Decidido pelo SO, tipicamente ~30 segundos | Vários segundos | Mais rápido |
Com everysec, se o fsync anterior ainda não terminou após 2 segundos por conta de disco lento, o Redis bloqueará comandos de escrita para evitar mais atraso. Por isso, a janela real de perda de dados pode ser de 2 segundos, não 1.
Também é importante distinguir dois tipos de falha. Se apenas o processo Redis travar, os dados já em write() continuam no cache do kernel e o sistema operacional os gravará no disco depois. Se a máquina inteira perder energia ou o kernel entrar em pânico, a parte que ainda não foi fsync será perdida.
3.2. AOF rewrite e AOF multi-part
AOF cresce continuamente. Por exemplo, 1 milhão de comandos INCR counter podem ser substituídos por um único SET counter 1000000. O Redis reescreve automaticamente o AOF quando o arquivo cresce o dobro desde o último rewrite (auto-aof-rewrite-percentage 100) e atinge o mínimo de 64 MB.
Desde o Redis 7, AOF fica em um diretório appendonlydir e consiste em múltiplos arquivos:
appendonlydir/
├── appendonly.aof.1.base.rdb # base, em formato RDB graças a aof-use-rdb-preamble
├── appendonly.aof.1.incr.aof # comandos de escrita gerados após base
└── appendonly.aof.manifest # descreve quais arquivos pertencem ao AOF atual
Com aof-use-rdb-preamble yes ativado, o arquivo base é gravado em formato RDB, então carrega rápido. A parte incr ainda é um log de comandos, então perde menos dados. Este é o modo “híbrido” e é o padrão desde o Redis 4.0.
4. Qual modo escolher?
| Necessidade | Configuração recomendada |
|---|---|
| Cache puro, dados reconstruíveis do DB | Desative tudo: save "", appendonly no |
| Aceita perder alguns minutos, precisa backup compacto | Apenas RDB |
| Session, carrinho de compras, rate limit que precisam ser duráveis | AOF everysec + RDB para backup |
| Não pode perder nenhum comando (raro) | AOF always, mas considere usar outro DB |
A escolha sensata para a maioria das aplicações é ativar ambos: AOF everysec para reduzir perda de dados, RDB para ter um arquivo de backup periódico. Quando ambos estão ativados, o Redis prefere carregar AOF na inicialização porque é mais completo.
Nota: se você usa Redis apenas como cache, desative persistência. Ativar RDB para cache só desperdiça
fork(), I/O de disco e RAM sem trazer nenhum benefício.
5. Verificando persistência a partir de uma aplicação Spring Boot
Adicione a dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
A classe a seguir lê o estado da persistência, solicita um snapshot e aguarda confirmação da replica:
@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;
}
}
Explicação de cada método:
persistenceInfo()chamaINFO persistencee retorna umPropertiescontendo campos comoaof_enabled,rdb_last_bgsave_status.triggerSnapshot()usaBGSAVE SCHEDULE. Se um rewrite de AOF está em andamento, o Redis agendará o snapshot em vez de retornar erro.waitForReplicas()chamaWAITe retorna quantas replicas confirmaram os comandos de escrita anteriores. A seção 6 explicará por que você precisa disso.
Uma parte da saída de INFO persistence com AOF ativado:
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
Três campos que você deve adicionar ao monitoramento e configurar alertas quando o valor não for ok: rdb_last_bgsave_status, aof_last_write_status e aof_last_bgrewrite_status. O campo rdb_changes_since_last_save mostra quanto de dados você perderá se contar apenas com RDB.
5.1. Teste com 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); // garante que o novo timestamp seja diferente do anterior
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);
}
}
O primeiro teste inicia o Redis 7.4 com AOF everysec e confirma que a configuração está em vigor. @ServiceConnection automaticamente aponta spring.data.redis.host/port para o container, então você não precisa declarar propriedades adicionais. O segundo teste aguarda até 5 segundos para rdb_last_save_time aumentar e depois verifica se o snapshot foi bem-sucedido.
6. Quando você ainda perde dados?
Ativar persistência não é suficiente. Abaixo estão as situações mais comuns em produção.
1. Crash entre dois snapshots. Se você usar apenas RDB, perde tudo escrito após rdb_last_save_time.
2. Janela de fsync. Com AOF everysec, você perde cerca de 1–2 segundos quando a máquina cai. Com no, esse número pode chegar a vários segundos.
3. Failover com replicação assíncrona. Master retorna OK ao cliente antes da replica receber o comando. Se o master cair e Sentinel ou Cluster promover a replica a master, os comandos que ainda não foram copiados desaparecem, mesmo com master tendo AOF always. WAIT ajuda a reduzir esse risco:
template.opsForValue().set("payment:7788", "CAPTURED");
long acked = inspector.waitForReplicas(1, 100);
if (acked < 1) {
log.warn("payment:7788 ainda não foi confirmado pela replica");
}
WAIT 1 100 bloqueia por no máximo 100 ms até que pelo menos 1 replica confirme. WAIT ainda não torna o Redis fortemente consistente: a replica pode receber o comando mas ainda não ter feito fsync. Veja isso como uma forma de reduzir a probabilidade de perda de dados, não como garantia.
4. Master reinicia com dataset vazio. Suponha que o master desative persistência, seja reiniciado automaticamente e suba novamente com dados vazios. A replica sincronizará com o master e limpará seus próprios dados. Se o master não tiver persistência ativada, não o deixe reiniciar automaticamente.
5. Container sem volume montado. Redis ainda grava dump.rdb e AOF, mas no filesystem do container. Quando o container é deletado e recriado, os arquivos se vão.
services:
redis:
image: redis:7.4
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis-data:/data
volumes:
redis-data:
Em Kubernetes, a situação equivalente é usar emptyDir em vez de PersistentVolumeClaim.
6. Disco cheio ou fork() falha. Quando BGSAVE falha e stop-writes-on-bgsave-error yes está ativado, o Redis rejeita todos os comandos de escrita com erro MISCONF. Esse comportamento protege os dados mas faz sua aplicação falhar. Muitos desativam essa opção e, a partir daí, o Redis continua sem persistência.
7. Acidentalmente executar FLUSHALL. Com AOF, o comando FLUSHALL é apenas uma linha no fim do arquivo incr. Se o AOF ainda não foi reescrito, você pode parar o Redis, remover essa linha e reiniciar. Se o rewrite já aconteceu, não há recuperação. Por isso, renomeie ou bloqueie esse comando com ACL em produção.
8. Eviction. Isso não é falha de persistência, mas o resultado é o mesmo. Com maxmemory-policy allkeys-lru, o Redis deleta chaves quando RAM está cheia. Dados que não podem ser perdidos precisam de noeviction.
Nota: não use Redis como o único lugar para armazenar dados de transações financeiras ou pedidos. Se você precisa garantir “retornar OK significa que não será perdido”, use um banco de dados relacional ou sistema com consensus. O Redis deve ser cache ou camada de aceleração.
7. Conclusão
RDB oferece um arquivo de backup compacto e reinicialização rápida, mas pode perder vários minutos de dados. AOF com everysec reduz a perda para cerca de 1–2 segundos, em troca de mais I/O. O modo híbrido (aof-use-rdb-preamble) combina as vantagens de ambos. Grande parte do risco real fica fora desses dois mecanismos: replicação assíncrona, volumes não montados, disco cheio, FLUSHALL e eviction. Monitore os campos em INFO persistence, use WAIT para comandos de escrita críticos e teste sua configuração com Testcontainers como mostrado na seção 5.