No desenvolvimento de sistemas de alta performance, o cache com Redis é crucial para aliviar a carga no banco de dados. Contudo, manter a consistência entre o cache e a fonte de dados é um desafio complexo. Inconsistências podem surgir de operações concorrentes ou falhas durante a atualização, resultando em dados obsoletos ou incorretos.
Causas e Manifestações da Inconsistência
A inconsistência ocorre quando uma atualização no banco de dados não é refletida corretamente no cache. Em ambientes distribuídos, a concorrência agrava o problema. As manifestações incluem:
- Leitura de dados sujos: O cache retorna valores antigos após uma atualização.
- Perda de atualizações: Uma sequência incorreta de operações faz com que dados atualizados sejam sobrescritos por versões desatualizadas no cache.
Estratégias de Atualização do Cache
Atualizar o Banco de Dados Primeiro
Esta abordagem falha sob concorrência. Se duas requisições atualizarem o banco de dados, a última a atualizar o cache determinará o valor final, potencialmente revertendo para uma versão anterior.
func AtualizarDBPrimeiro(rdb *redis.Client, chave string, valorDB string) error {
// Passo 1: Atualizar o banco de dados
fmt.Println("Atualizando banco de dados...")
// (Lógica de atualização do DB)
// Passo 2: Atualizar o cache
return rdb.Set(context.Background(), chave, valorDB, 0).Err()
}
Atualizar o Cache Primeiro
Ao atualizar o cache primeiro, uma falha subsequente no banco de dados deixa o sistema com dados cacheados inválidos.
func AtualizarCachePrimeiro(rdb *redis.Client, chave string, valorNovo string) error {
// Passo 1: Atualizar o cache
if err := rdb.Set(context.Background(), chave, valorNovo, 0).Err(); err != nil {
return err
}
// Passo 2: Atualizar o banco de dados
fmt.Println("Atualizando banco de dados...")
// (Lógica de atualização do DB)
return nil
}
Excluir o Cache Primeiro
Excluir antes de atualizra o banco de dados pode levar à regeneração do cache com dados antigos se uma leitura ocorrer entre a exclusão e a atualização.
Atualizar o Banco e Depois Excluir o Cache (Padrão Comum)
Esta é a abordagem mais robusta das estratégias básicas. A leitura subsequente buscará dados frescos do banco. O risco principal é a falha na etapa de exclusão do cache.
func AtualizarDBExcluirCache(rdb *redis.Client, chave string) error {
// Passo 1: Atualizar o banco de dados
fmt.Println("Atualizando banco de dados...")
// (Lógica de atualização do DB)
// Passo 2: Invalidar o cache
return rdb.Del(context.Background(), chave).Err()
}
Estratégias Avançadas
Controle de Concorrrência com Locks
Utilizar um RWMutex garante que apenas uma escrita ocorra por vez, enquanto múltiplas leituras podem ser concorrentes.
var cacheLocal = make(map[string]string)
var rwLock = sync.RWMutex{}
func EscreverComLock(chave, valor string) {
rwLock.Lock()
defer rwLock.Unlock()
cacheLocal[chave] = valor
}
func LerComLock(chave string) (string, bool) {
rwLock.RLock()
defer rwLock.RUnlock()
valor, existe := cacheLocal[chave]
return valor, existe
}
Exclusão Dupla Atrasada
Para mitigar o problema de leituras concorrentes regenerando o cache com dados antigos após uma única exclusão, esta estratégia realiza uma segunda exclusão após um atraso.
- Exclui o cache.
- Atualiza o banco de dados.
- Agenda uma segunda exclusão do cache após um intervalo (ex: 500ms).
func AtualizarComExclusaoDuplaAtrasada(rdb *redis.Client, chave string, valorDB string, atraso time.Duration) error {
ctx := context.Background()
// 1. Primeira exclusão
if err := rdb.Del(ctx, chave).Err(); err != nil {
return fmt.Errorf("falha na primeira exclusão: %w", err)
}
// 2. Atualizar banco de dados
fmt.Println("Atualizando banco de dados...")
// (Lógica de atualização do DB)
// 3. Segunda exclusão assíncrona
go func() {
time.Sleep(atraso)
if err := rdb.Del(ctx, chave).Err(); err != nil {
fmt.Printf("falha na segunda exclusão: %v\n", err)
}
}()
return nil
}
Esta técnica é eficaz em cenarios com alta conncorrência de leitura logo após uma escrita, como na atualização de estoque de produtos.
Versionamento no Cache
Armazenar um número de versão junto com o dado no cache permite validar sua atualidade.
// Atualiza dados com versionamento
func AtualizarComVersao(rdb *redis.Client, chave string, valor string, versao int) error {
dados := map[string]interface{}{
"dados": valor,
"versao": versao,
}
return rdb.HSet(context.Background(), chave, dados).Err()
}
// Lê e verifica a versão
func LerComVerificao(rdb *redis.Client, chave string, versaoEsperada int) (string, error) {
resultado, err := rdb.HGetAll(context.Background(), chave).Result()
if err != nil {
return "", err
}
versaoCache, _ := strconv.Atoi(resultado["versao"])
if versaoCache < versaoEsperada {
// Cache desatualizado, buscar do DB
valorDB := buscarDoBancoDeDados()
// Atualizar cache com nova versão...
return valorDB, nil
}
return resultado["dados"], nil
}