Garantindo Consistência no Cache com Redis: Técnicas e Estratégias

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.

  1. Exclui o cache.
  2. Atualiza o banco de dados.
  3. 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
}

Tags: Redis go Cache Distribuído Padrões de Consistência Sistemas de Alta Performance

Publicado em 6-4 20:02 por Thomas