Go Admin

Redis com Go: Cache, Pub/Sub e Filas com go-redis na Prática Já leu

Entendendo Redis e a Biblioteca go-redis Redis é um armazenamento de dados em memória extremamente rápido, baseado em estruturas de dados chave-valor. Diferentemente de bancos de dados tradicionais, Redis mantém todos os dados na RAM, o que o torna ideal para casos onde a velocidade é crítica: caches, filas de processamento, sessões de usuários e comunicação em tempo real entre aplicações. A biblioteca é um cliente Go oficial que nos permite interagir com Redis de forma simples e idiomática. Ela fornece abstrações para operações básicas (GET, SET), estruturas de dados (Strings, Lists, Hashes, Sets, Sorted Sets), Pub/Sub para comunicação entre serviços, e suporte a Lua scripting. Neste artigo, exploraremos três casos de uso fundamentais: caching para otimizar aplicações, Pub/Sub para mensageria em tempo real, e filas para processamento assíncrono de tarefas. Instalação e Configuração Básica Preparando o Ambiente Primeiro, você precisa ter Redis instalado e rodando. No Linux, use . No macOS, utilize . Você pode verificar se está funcionando

Entendendo Redis e a Biblioteca go-redis

Redis é um armazenamento de dados em memória extremamente rápido, baseado em estruturas de dados chave-valor. Diferentemente de bancos de dados tradicionais, Redis mantém todos os dados na RAM, o que o torna ideal para casos onde a velocidade é crítica: caches, filas de processamento, sessões de usuários e comunicação em tempo real entre aplicações.

A biblioteca go-redis é um cliente Go oficial que nos permite interagir com Redis de forma simples e idiomática. Ela fornece abstrações para operações básicas (GET, SET), estruturas de dados (Strings, Lists, Hashes, Sets, Sorted Sets), Pub/Sub para comunicação entre serviços, e suporte a Lua scripting. Neste artigo, exploraremos três casos de uso fundamentais: caching para otimizar aplicações, Pub/Sub para mensageria em tempo real, e filas para processamento assíncrono de tarefas.

Instalação e Configuração Básica

Preparando o Ambiente

Primeiro, você precisa ter Redis instalado e rodando. No Linux, use sudo apt-get install redis-server. No macOS, utilize brew install redis. Você pode verificar se está funcionando com redis-cli ping, que deve retornar PONG.

Em seu projeto Go, instale a biblioteca go-redis:

go get github.com/redis/go-redis/v9

Conectando ao Redis

A conexão é o primeiro passo. Você cria um cliente que mantém um pool de conexões interno e o reutiliza. O contexto (context.Context) é fundamental em Go para controle de timeouts e cancelamentos:

package main

import (
    "context"
    "fmt"
    "github.com/redis/go-redis/v9"
)

func main() {
    // Conectar ao Redis rodando em localhost:6379
    client := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
    defer client.Close()

    // Verificar conexão
    ctx := context.Background()
    pong, err := client.Ping(ctx).Result()
    if err != nil {
        panic(err)
    }
    fmt.Println(pong) // Output: PONG
}

Observe que criamos um contexto com context.Background(), que é usado como base. Em aplicações reais, você passará contextos com timeouts para operações específicas. A função defer client.Close() garante que a conexão seja fechada quando a função terminar.

Cache de Dados com Redis

O Conceito de Cache

Um cache é uma camada de armazenamento rápido entre sua aplicação e a fonte de dados (banco de dados, API externa). Quando uma requisição chega, você primeiro verifica se o dado está no cache. Se estiver ("cache hit"), retorna imediatamente. Se não estiver ("cache miss"), você busca da fonte original, armazena no cache e retorna. Isso reduz latência e carga no banco de dados.

Implementando um Cache Simples

Vamos criar uma função que busca dados de um usuário. Se estiver no cache, devolve de lá; caso contrário, simula uma busca em banco de dados:

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "github.com/redis/go-redis/v9"
    "time"
)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
    Email string `json:"email"`
}

// Simula uma busca custosa em banco de dados
func fetchUserFromDB(userID int) *User {
    time.Sleep(2 * time.Second) // Simula latência
    return &User{
        ID:    userID,
        Name:  "João Silva",
        Email: "joao@example.com",
    }
}

// GetUser implementa cache com fallback
func GetUser(client *redis.Client, ctx context.Context, userID int) (*User, error) {
    key := fmt.Sprintf("user:%d", userID)

    // Tenta recuperar do cache
    cachedData, err := client.Get(ctx, key).Result()
    if err == nil {
        // Cache hit
        var user User
        json.Unmarshal([]byte(cachedData), &user)
        fmt.Println("Recuperado do cache")
        return &user, nil
    }

    // Cache miss: busca do banco
    fmt.Println("Recuperado do banco de dados")
    user := fetchUserFromDB(userID)

    // Armazena no cache com TTL de 1 hora
    userJSON, _ := json.Marshal(user)
    client.Set(ctx, key, userJSON, 1*time.Hour)

    return user, nil
}

func main() {
    client := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
    defer client.Close()

    ctx := context.Background()

    // Primeira chamada: vai para o banco
    start := time.Now()
    user1, _ := GetUser(client, ctx, 1)
    fmt.Printf("User: %v | Tempo: %v\n\n", user1.Name, time.Since(start))

    // Segunda chamada: vem do cache (muito mais rápida)
    start = time.Now()
    user2, _ := GetUser(client, ctx, 1)
    fmt.Printf("User: %v | Tempo: %v\n", user2.Name, time.Since(start))
}

Neste exemplo, a primeira chamada demora ~2 segundos (simulação do banco). A segunda chamada retorna em milissegundos do cache. Usamos Set(ctx, key, value, expiration) para armazenar com um TTL (time-to-live), garantindo que dados antigos sejam removidos automaticamente.

Cache com Padrão Cache-Aside Avançado

Em sistemas complexos, você pode precisar invalidar o cache quando dados são atualizados. Veja um exemplo com operação de atualização:

// UpdateUser atualiza o usuário e limpa o cache
func UpdateUser(client *redis.Client, ctx context.Context, user *User) error {
    // Atualiza no banco (simulado aqui)
    fmt.Println("Usuário atualizado no banco")

    // Invalida o cache
    key := fmt.Sprintf("user:%d", user.ID)
    err := client.Del(ctx, key).Err()
    if err != nil {
        return err
    }
    fmt.Println("Cache invalidado")
    return nil
}

Pub/Sub: Comunicação em Tempo Real

Entendendo o Padrão Publish/Subscribe

Pub/Sub é um padrão de mensageria onde um publisher envia mensagens para um canal, e múltiplos subscribers escutam aquele canal. É útil para notificações em tempo real, eventos do sistema, ou coordenação entre microserviços. Diferentemente de filas, Pub/Sub não persiste mensagens—se ninguém estiver escutando, a mensagem é perdida.

Implementando Publisher e Subscriber

Vamos criar um exemplo de notificações de pedidos. Um serviço publica quando um pedido é criado, e outros serviços recebem essa notificação:

package main

import (
    "context"
    "fmt"
    "github.com/redis/go-redis/v9"
    "time"
)

// Publisher envia mensagens para um canal
func PublishOrderNotification(client *redis.Client, ctx context.Context) {
    channel := "orders:new"
    messages := []string{
        "Pedido #1001 criado",
        "Pedido #1002 criado",
        "Pedido #1003 criado",
    }

    for i, msg := range messages {
        // Publica a mensagem
        err := client.Publish(ctx, channel, msg).Err()
        if err != nil {
            fmt.Printf("Erro ao publicar: %v\n", err)
        }
        fmt.Printf("[PUB] %s\n", msg)
        time.Sleep(1 * time.Second)
    }
}

// Subscriber escuta mensagens de um canal
func SubscribeOrderNotifications(client *redis.Client, ctx context.Context, name string) {
    channel := "orders:new"
    sub := client.Subscribe(ctx, channel)
    defer sub.Close()

    // Cria um canal Go para receber mensagens
    ch := sub.Channel()

    fmt.Printf("[%s] Escutando no canal '%s'...\n", name, channel)
    for i := 0; i < 3; i++ {
        msg := <-ch
        fmt.Printf("[%s] Recebido: %s\n", name, msg.Payload)
    }
}

func main() {
    client := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
    defer client.Close()

    ctx := context.Background()

    // Inicia subscribers em goroutines
    go SubscribeOrderNotifications(client, ctx, "Subscriber-1")
    go SubscribeOrderNotifications(client, ctx, "Subscriber-2")

    // Aguarda um pouco para subscribers se registrarem
    time.Sleep(500 * time.Millisecond)

    // Publisher envia mensagens
    PublishOrderNotification(client, ctx)

    time.Sleep(2 * time.Second)
}

Execute este código em dois terminais ou em goroutines. Os subscribers precisam estar escutando antes das mensagens serem publicadas. Quando você executa, verá que múltiplos subscribers recebem a mesma mensagem simultaneamente—esse é o poder do Pub/Sub.

Padrão Subscribe com Pattern

Redis também permite inscrever-se em padrões. Por exemplo, orders:* capturaria todas as mensagens de canais começando com "orders:":

// Subscrever com padrão
pSub := client.PSubscribe(ctx, "orders:*", "notifications:*")
defer pSub.Close()

ch := pSub.Channel()
for msg := range ch {
    fmt.Printf("Canal: %s, Mensagem: %s\n", msg.Channel, msg.Payload)
}

Filas de Processamento Assíncrono

Por Que Usar Filas?

Filas desacoplam produtores de consumidores. Um serviço coloca tarefas na fila (enqueue), e workers as processam (dequeue) em seu próprio ritmo. Diferentemente de Pub/Sub, filas persistem mensagens até que sejam consumidas. Você pode ter múltiplos workers processando a mesma fila, escalando facilmente.

Implementando Fila com LPUSH e RPOP

Redis oferece operações de lista para implementar filas. LPUSH adiciona à esquerda, RPOP remove da direita (FIFO):

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "github.com/redis/go-redis/v9"
    "time"
)

type Task struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
    Data string `json:"data"`
}

// EnqueueTask adiciona uma tarefa à fila
func EnqueueTask(client *redis.Client, ctx context.Context, task *Task) error {
    queueKey := "tasks:queue"
    taskJSON, _ := json.Marshal(task)

    // Adiciona à esquerda (LPUSH)
    err := client.LPush(ctx, queueKey, taskJSON).Err()
    if err != nil {
        return err
    }
    fmt.Printf("Tarefa enfileirada: ID=%d, Nome=%s\n", task.ID, task.Name)
    return nil
}

// Worker processa tarefas da fila
func Worker(client *redis.Client, ctx context.Context, workerID int) {
    queueKey := "tasks:queue"

    for {
        // Tenta desenfila uma tarefa (RPOP com timeout)
        result, err := client.BRPop(ctx, 5*time.Second, queueKey).Result()
        if err != nil {
            if err == redis.Nil {
                // Timeout: nenhuma tarefa disponível
                fmt.Printf("[Worker-%d] Nenhuma tarefa, aguardando...\n", workerID)
                continue
            }
            fmt.Printf("[Worker-%d] Erro: %v\n", workerID, err)
            break
        }

        // Decodifica a tarefa
        var task Task
        json.Unmarshal([]byte(result[1]), &task)

        // Processa
        fmt.Printf("[Worker-%d] Processando: ID=%d, Nome=%s\n", workerID, task.ID, task.Name)
        time.Sleep(1 * time.Second) // Simula processamento
        fmt.Printf("[Worker-%d] Tarefa concluída: ID=%d\n", workerID, task.ID)
    }
}

func main() {
    client := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
    defer client.Close()

    ctx := context.Background()

    // Limpa a fila (opcional)
    client.Del(ctx, "tasks:queue")

    // Inicia 2 workers
    go Worker(client, ctx, 1)
    go Worker(client, ctx, 2)

    time.Sleep(500 * time.Millisecond)

    // Produtor enfileira tarefas
    for i := 1; i <= 5; i++ {
        task := &Task{
            ID:   i,
            Name: fmt.Sprintf("Tarefa %d", i),
            Data: "dados importantes",
        }
        EnqueueTask(client, ctx, task)
        time.Sleep(200 * time.Millisecond)
    }

    time.Sleep(10 * time.Second)
}

Neste exemplo, BRPop (Blocking Right Pop) aguarda por um timeout se a fila estiver vazia. Isso é muito mais eficiente que ficar verificando (polling). Múltiplos workers podem processar a mesma fila simultaneamente—cada um recebe tarefas diferentes.

Fila Robusta com Retry

Em produção, tarefas podem falhar. Uma abordagem é usar uma fila de "dead letter" para tarefas que falharam múltiplas vezes:

// ProcessTaskWithRetry processa uma tarefa com suporte a retry
func ProcessTaskWithRetry(client *redis.Client, ctx context.Context, task *Task, maxRetries int) error {
    retryKey := fmt.Sprintf("task:retry:%d", task.ID)

    retries, _ := client.Get(ctx, retryKey).Int()

    // Tenta processar
    err := ProcessTask(task) // sua lógica de processamento

    if err != nil {
        if retries < maxRetries {
            // Re-enfileira
            retries++
            client.Set(ctx, retryKey, retries, 24*time.Hour)
            EnqueueTask(client, ctx, task)
            fmt.Printf("Tarefa %d re-enfileirada (tentativa %d)\n", task.ID, retries)
        } else {
            // Envia para dead letter queue
            deadLetterKey := "tasks:dead_letter"
            taskJSON, _ := json.Marshal(task)
            client.LPush(ctx, deadLetterKey, taskJSON)
            fmt.Printf("Tarefa %d descartada (dead letter)\n", task.ID)
        }
        return err
    }

    // Sucesso: limpa contador de retry
    client.Del(ctx, retryKey)
    return nil
}

func ProcessTask(task *Task) error {
    // Lógica real de processamento
    return nil
}

Estratégias de Otimização e Boas Práticas

Pipelining para Múltiplas Operações

Quando você precisa fazer várias operações, pipelinning reduz latência agrupando-as em uma única chamada de rede:

func BatchUpdateCache(client *redis.Client, ctx context.Context, users []User) error {
    pipe := client.Pipeline()

    for _, user := range users {
        key := fmt.Sprintf("user:%d", user.ID)
        userJSON, _ := json.Marshal(user)
        pipe.Set(ctx, key, userJSON, 1*time.Hour)
    }

    // Executa todas as operações de uma vez
    _, err := pipe.Exec(ctx)
    return err
}

Monitorando Conexões

Em aplicações que lidam com muito tráfego, monitore a saúde da conexão:

// Health check
func HealthCheck(client *redis.Client, ctx context.Context) error {
    _, err := client.Ping(ctx).Result()
    if err != nil {
        fmt.Printf("Redis indisponível: %v\n", err)
        return err
    }
    fmt.Println("Redis OK")
    return nil
}

// Em sua aplicação, chame periodicamente
go func() {
    ticker := time.NewTicker(30 * time.Second)
    for range ticker.C {
        HealthCheck(client, context.Background())
    }
}()

Evitar Chaves Grandes

Armazenar objetos muito grandes em uma única chave degrada performance. Prefira normalizar—guarde referências em uma estrutura de índice:

// Ruim: uma chave gigante com lista de todos os usuários
// client.Set(ctx, "all:users", largeJSON, time.Hour)

// Bom: usar Set ou Hash
client.SAdd(ctx, "users:ids", "1", "2", "3")
// E armazenar cada usuário separadamente
client.Set(ctx, "user:1", userJSON, time.Hour)

Conclusão

Durante este artigo, cobrimos três pilares da integração Redis com Go. Primeiro, o cache com go-redis reduz drasticamente latência ao armazenar dados acessados frequentemente, com suporte automático a TTL para evitar dados obsoletos. Segundo, Pub/Sub permite comunicação em tempo real entre componentes de um sistema, escalando para múltiplos subscribers sem overhead. Terceiro, filas implementadas com operações de lista (LPUSH/RPOP ou BRPOP) desacoplam produtores e consumidores, permitindo processamento assíncrono e escalável de tarefas.

A escolha entre esses padrões depende do seu caso de uso: use cache para dados que mudam infrequentemente, Pub/Sub para notificações em tempo real, e filas para tarefas que podem ser processadas assincronamente. Go-redis facilita todas essas implementações com uma API limpa, suporte a contextos para controle fino de timeouts, e pipelining para otimização. Pratique com exemplos reais—um cache bem calibrado pode reduzir carga do banco de dados em 10x, e filas bem gerenciadas transformam sistemas síncronos em robustos processadores de eventos.

Referências


Artigos relacionados