M02·05Cache com Redis

CAPÍTULO 05

Cache com Redis

Quando usar cache, o padrão Cache-Aside, e como implementar invalidação sem deixar dado velho em produção.

Por Thiago Souza7 min de leituraAtualizado em 2026-05

O que é cache?

Analogia da geladeira: quando você quer um copo d'água, vai na geladeira (rápido). Quando a água acaba, você vai no mercado (lento). A geladeira é o cache: tem menos coisa, mas está perto e é rápido.

No backend, banco é o mercado e Redis é a geladeira. Você guarda no Redis aquilo que:

  • É lido muito mais do que escrito.
  • É caro de calcular ou de buscar (query pesada, chamada externa).
  • Pode estar um pouco desatualizado sem o mundo cair.

Quando usar (e quando NÃO usar)

Use cache quando:

  • Listagens populares (top 10 produtos, ranking).
  • Dados de configuração que mudam pouco.
  • Resultados de cálculos pesados.
  • Sessão de usuário, rate limiting, locks distribuídos.

NÃO use cache quando:

  • O dado precisa ser sempre exato e atual (saldo bancário no momento da transação).
  • A complexidade de manter o cache consistente é maior que o ganho.
  • O dado é único por usuário e raramente lido de novo.

Existem dois problemas difíceis em computação: invalidar cache e nomear coisas. Cache só parece simples até a primeira vez que alguém vê dado desatualizado em produção.

Padrão Cache-Aside (o mais comum)

1. Pede ao service: "me dá user 123"
2. Service olha o Redis. Tem? Devolve.
3. Não tem? Vai no banco, pega o dado.
4. Coloca o dado no Redis (com TTL).
5. Devolve.

Implementação em Go usando go-redis:

go-redis/v9 requer Go 1.24 ou superior. Certifique-se que seu go.mod declara go 1.24 ou acima antes de instalar.
go
package user
 
import (
    "context"
    "encoding/json"
    "errors"
    "time"
    "github.com/redis/go-redis/v9"
)
 
type CachedRepo struct {
    next  Repository      // o repositório "real" (postgres)
    cache *redis.Client
    ttl   time.Duration
}
 
func NewCachedRepo(next Repository, cache *redis.Client) *CachedRepo {
    return &CachedRepo{next: next, cache: cache, ttl: 5 * time.Minute}
}
 
func (r *CachedRepo) FindByID(ctx context.Context, id int64) (User, error) {
    key := fmt.Sprintf("user:%d", id)
 
    // 1. Tenta o cache primeiro.
    if data, err := r.cache.Get(ctx, key).Bytes(); err == nil {
        var u User
        if err := json.Unmarshal(data, &u); err == nil {
            return u, nil  // cache hit, voltamos rapidinho
        }
    }
 
    // 2. Cache miss: busca no banco.
    u, err := r.next.FindByID(ctx, id)
    if err != nil {
        return User{}, err
    }
 
    // 3. Guarda no cache para próxima.
    //    Erro aqui NÃO derruba a request — log e segue.
    if data, err := json.Marshal(u); err == nil {
        _ = r.cache.Set(ctx, key, data, r.ttl).Err()
    }
 
    return u, nil
}

Note como o CachedRepo implementa a mesma interface Repository. O service nem fica sabendo que tem cache no meio do caminho. Isso é decoração, e é uma das técnicas mais elegantes de arquitetura.

Cuidado com invalidação

Quando você atualizar ou deletar um usuário, lembre de apagar a chave do cache correspondente, ou alguém vai ver dado velho.

go
func (r *CachedRepo) Update(ctx context.Context, u User) error {
    if err := r.next.Update(ctx, u); err != nil {
        return err
    }
    _ = r.cache.Del(ctx, fmt.Sprintf("user:%d", u.ID)).Err()
    return nil
}