Go e Redis: Cache e Session Store Completo

O Redis é um dos bancos de dados em memória mais populares do mundo, e quando combinado com Go, cria aplicações extremamente rápidas e escaláveis. Neste guia completo, você vai aprender a implementar caching de alta performance e gerenciamento de sessões usando Go e Redis.

Por Que Usar Redis com Go?

Antes de mergulhar no código, entenda por que essa combinação é tão poderosa:

RecursoBenefício
LatênciaSub-milissegundo para operações simples
Throughput100.000+ operações por segundo
EstruturasStrings, Hashes, Lists, Sets, Sorted Sets
PersistênciaOpcional (RDB snapshots, AOF logs)
Pub/SubMensageria em tempo real
Go Drivergo-redis é maduro e bem mantido

Caso Real: Empresas como Twitter, GitHub e Stack Overflow usam Redis para caching, reduzindo drasticamente a carga em seus bancos de dados principais.

Configurando o Redis Client em Go

Instalação

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

Conexão Básica

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/redis/go-redis/v9"
)

var ctx = context.Background()

func main() {
    // Configuração do cliente Redis
    rdb := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "", // sem senha
        DB:       0,  // banco de dados padrão
        PoolSize: 10, // tamanho do pool de conexões
    })

    // Testa a conexão
    pong, err := rdb.Ping(ctx).Result()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("Conectado:", pong)

    defer rdb.Close()
}

Configuração para Produção

rdb := redis.NewClient(&redis.Options{
    Addr:         "redis.example.com:6379",
    Password:     os.Getenv("REDIS_PASSWORD"),
    DB:           0,
    MaxRetries:   3,
    DialTimeout:  5 * time.Second,
    ReadTimeout:  3 * time.Second,
    WriteTimeout: 3 * time.Second,
    PoolSize:     20,
    MinIdleConns: 5,
})

Padrões de Caching em Go

1. Cache-Aside (Lazy Loading)

O padrão mais comum: verifica o cache primeiro, busca no banco se não existir.

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

func getUser(ctx context.Context, rdb *redis.Client, userID int) (*User, error) {
    cacheKey := fmt.Sprintf("user:%d", userID)
    
    // 1. Tenta buscar do cache
    cached, err := rdb.Get(ctx, cacheKey).Result()
    if err == nil {
        var user User
        if err := json.Unmarshal([]byte(cached), &user); err == nil {
            fmt.Println("Cache hit!")
            return &user, nil
        }
    }
    
    // 2. Cache miss - busca do banco de dados
    user, err := fetchUserFromDB(userID)
    if err != nil {
        return nil, err
    }
    
    // 3. Armazena no cache para próximas requisições
    userJSON, _ := json.Marshal(user)
    err = rdb.Set(ctx, cacheKey, userJSON, 5*time.Minute).Err()
    if err != nil {
        log.Printf("Erro ao salvar no cache: %v", err)
    }
    
    return user, nil
}

2. Write-Through Cache

Atualiza o cache simultaneamente com o banco de dados.

func updateUser(ctx context.Context, rdb *redis.Client, user *User) error {
    // 1. Atualiza o banco de dados
    if err := updateUserInDB(user); err != nil {
        return err
    }
    
    // 2. Atualiza o cache
    cacheKey := fmt.Sprintf("user:%d", user.ID)
    userJSON, _ := json.Marshal(user)
    
    if err := rdb.Set(ctx, cacheKey, userJSON, 5*time.Minute).Err(); err != nil {
        log.Printf("Erro ao atualizar cache: %v", err)
        // Não retorna erro - cache pode ser reconstruído
    }
    
    return nil
}

3. Cache com TTL (Time To Live)

Define expiração automática para evitar dados desatualizados.

// Cache com diferentes TTLs baseado no tipo de dado
func setWithTTL(ctx context.Context, rdb *redis.Client, key string, value interface{}, ttl time.Duration) error {
    data, err := json.Marshal(value)
    if err != nil {
        return err
    }
    return rdb.Set(ctx, key, data, ttl).Err()
}

// Uso
setWithTTL(ctx, rdb, "config:app", config, 1*time.Hour)      // Configurações: 1h
setWithTTL(ctx, rdb, "user:123", user, 15*time.Minute)       // Usuários: 15min
setWithTTL(ctx, rdb, "session:abc", session, 24*time.Hour)   // Sessões: 24h

Gerenciamento de Sessões com Redis

Estrutura de Sessão

type Session struct {
    UserID    int       `json:"user_id"`
    Username  string    `json:"username"`
    Data      map[string]interface{} `json:"data"`
    CreatedAt time.Time `json:"created_at"`
}

type SessionManager struct {
    rdb       *redis.Client
    ctx       context.Context
    ttl       time.Duration
}

func NewSessionManager(rdb *redis.Client, ttl time.Duration) *SessionManager {
    return &SessionManager{
        rdb: rdb,
        ctx: context.Background(),
        ttl: ttl,
    }
}

Criar Sessão

func (sm *SessionManager) CreateSession(userID int, username string) (string, error) {
    sessionID := generateSessionID() // UUID ou token seguro
    
    session := Session{
        UserID:    userID,
        Username:  username,
        Data:      make(map[string]interface{}),
        CreatedAt: time.Now(),
    }
    
    sessionJSON, err := json.Marshal(session)
    if err != nil {
        return "", err
    }
    
    key := fmt.Sprintf("session:%s", sessionID)
    if err := sm.rdb.Set(sm.ctx, key, sessionJSON, sm.ttl).Err(); err != nil {
        return "", err
    }
    
    // Mantém índice de sessões por usuário
    userSessionsKey := fmt.Sprintf("user:sessions:%d", userID)
    sm.rdb.SAdd(sm.ctx, userSessionsKey, sessionID)
    sm.rdb.Expire(sm.ctx, userSessionsKey, sm.ttl)
    
    return sessionID, nil
}

Validar e Recuperar Sessão

func (sm *SessionManager) GetSession(sessionID string) (*Session, error) {
    key := fmt.Sprintf("session:%s", sessionID)
    
    data, err := sm.rdb.Get(sm.ctx, key).Result()
    if err == redis.Nil {
        return nil, fmt.Errorf("sessão não encontrada ou expirada")
    }
    if err != nil {
        return nil, err
    }
    
    var session Session
    if err := json.Unmarshal([]byte(data), &session); err != nil {
        return nil, err
    }
    
    // Renova TTL (sliding expiration)
    sm.rdb.Expire(sm.ctx, key, sm.ttl)
    
    return &session, nil
}

func (sm *SessionManager) DestroySession(sessionID string) error {
    session, err := sm.GetSession(sessionID)
    if err != nil {
        return err
    }
    
    // Remove sessão principal
    key := fmt.Sprintf("session:%s", sessionID)
    if err := sm.rdb.Del(sm.ctx, key).Err(); err != nil {
        return err
    }
    
    // Remove do índice do usuário
    userSessionsKey := fmt.Sprintf("user:sessions:%d", session.UserID)
    sm.rdb.SRem(sm.ctx, userSessionsKey, sessionID)
    
    return nil
}

Middleware de Autenticação (Gin/Gorilla)

func AuthMiddleware(sm *SessionManager) gin.HandlerFunc {
    return func(c *gin.Context) {
        sessionID, err := c.Cookie("session_id")
        if err != nil {
            c.JSON(401, gin.H{"error": "não autenticado"})
            c.Abort()
            return
        }
        
        session, err := sm.GetSession(sessionID)
        if err != nil {
            c.JSON(401, gin.H{"error": "sessão inválida"})
            c.Abort()
            return
        }
        
        // Disponibiliza dados da sessão no contexto
        c.Set("user_id", session.UserID)
        c.Set("username", session.Username)
        c.Set("session", session)
        
        c.Next()
    }
}

Pub/Sub com Redis em Go

Implemente comunicação em tempo real entre serviços.

Publisher

func publishMessage(rdb *redis.Client, channel string, message string) error {
    return rdb.Publish(ctx, channel, message).Err()
}

// Uso
publishMessage(rdb, "notifications", `{"type":"new_order","order_id":123}`)

Subscriber

func subscribeToChannel(rdb *redis.Client, channel string) {
    pubsub := rdb.Subscribe(ctx, channel)
    defer pubsub.Close()
    
    // Verifica conexão
    if _, err := pubsub.Receive(ctx); err != nil {
        log.Fatal(err)
    }
    
    // Canal para mensagens
    ch := pubsub.Channel()
    
    fmt.Printf("Inscrito no canal: %s\n", channel)
    
    for msg := range ch {
        fmt.Printf("Canal: %s | Mensagem: %s\n", msg.Channel, msg.Payload)
        
        // Processa mensagem
        processMessage(msg.Payload)
    }
}

func processMessage(payload string) {
    var notification map[string]interface{}
    if err := json.Unmarshal([]byte(payload), &notification); err != nil {
        log.Printf("Erro ao parsear mensagem: %v", err)
        return
    }
    
    // Lógica de processamento
    fmt.Printf("Processando notificação: %+v\n", notification)
}

Pattern Matching em Pub/Sub

// Inscreve em múltiplos canais com padrão
pubsub := rdb.PSubscribe(ctx, "user:*:notifications")

ch := pubsub.Channel()
for msg := range ch {
    // Recebe mensagens de user:123:notifications, user:456:notifications, etc.
    fmt.Printf("Pattern: %s | Channel: %s\n", msg.Pattern, msg.Channel)
}

Otimização de Performance

1. Pipeline para Operações em Lote

Reduzida round-trips de rede:

func batchOperations(rdb *redis.Client) error {
    pipe := rdb.Pipeline()
    
    // Enfileira operações
    pipe.Set(ctx, "key1", "value1", 0)
    pipe.Set(ctx, "key2", "value2", 0)
    pipe.Get(ctx, "key1")
    pipe.Incr(ctx, "counter")
    
    // Executa todas de uma vez
    cmders, err := pipe.Exec(ctx)
    if err != nil {
        return err
    }
    
    // Processa resultados
    for _, cmder := range cmders {
        fmt.Println(cmder)
    }
    
    return nil
}

2. Transactions (MULTI/EXEC)

func transactionalUpdate(rdb *redis.Client, userID int, newBalance float64) error {
    key := fmt.Sprintf("balance:%d", userID)
    
    err := rdb.Watch(ctx, func(tx *redis.Tx) error {
        // Verifica saldo atual
        currentBalance, err := tx.Get(ctx, key).Float64()
        if err != nil && err != redis.Nil {
            return err
        }
        
        if newBalance < 0 && currentBalance < -newBalance {
            return fmt.Errorf("saldo insuficiente")
        }
        
        // Executa transação
        _, err = tx.TxPipelined(ctx, func(pipe redis.Pipeliner) error {
            pipe.Set(ctx, key, currentBalance+newBalance, 0)
            pipe.Incr(ctx, "transactions:count")
            return nil
        })
        
        return err
    }, key)
    
    return err
}

3. Connection Pooling

O go-redis gerencia automaticamente, mas você pode ajustar:

rdb := redis.NewClient(&redis.Options{
    PoolSize:     30,              // Conexões máximas
    MinIdleConns: 10,              // Conexões ociosas mínimas
    MaxConnAge:   time.Hour,       // Tempo máximo de vida da conexão
    PoolTimeout:  5 * time.Second, // Timeout para obter conexão do pool
    IdleTimeout:  10 * time.Minute,// Timeout para conexões ociosas
})

4. Compressão de Dados

Para objetos grandes, use compressão:

import "github.com/klauspost/compress/zstd"

func setCompressed(ctx context.Context, rdb *redis.Client, key string, data interface{}) error {
    jsonData, err := json.Marshal(data)
    if err != nil {
        return err
    }
    
    // Comprime com zstd
    encoder, _ := zstd.NewWriter(nil)
    compressed := encoder.EncodeAll(jsonData, make([]byte, 0, len(jsonData)))
    
    return rdb.Set(ctx, key, compressed, 0).Err()
}

Exemplo Completo: API com Caching

package main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/redis/go-redis/v9"
)

type Product struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

type CachedRepository struct {
    rdb *redis.Client
    ctx context.Context
}

func NewCachedRepository(rdb *redis.Client) *CachedRepository {
    return &CachedRepository{
        rdb: rdb,
        ctx: context.Background(),
    }
}

func (r *CachedRepository) GetProduct(id int) (*Product, error) {
    cacheKey := fmt.Sprintf("product:%d", id)
    
    // Tenta cache
    cached, err := r.rdb.Get(r.ctx, cacheKey).Result()
    if err == nil {
        var p Product
        json.Unmarshal([]byte(cached), &p)
        return &p, nil
    }
    
    // Simula busca no banco
    product := &Product{
        ID:    id,
        Name:  fmt.Sprintf("Produto %d", id),
        Price: float64(id) * 10.99,
    }
    
    // Salva no cache
    data, _ := json.Marshal(product)
    r.rdb.Set(r.ctx, cacheKey, data, 10*time.Minute)
    
    return product, nil
}

func (r *CachedRepository) InvalidateProduct(id int) {
    cacheKey := fmt.Sprintf("product:%d", id)
    r.rdb.Del(r.ctx, cacheKey)
}

func main() {
    // Redis client
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })
    defer rdb.Close()
    
    repo := NewCachedRepository(rdb)
    
    // API
    r := gin.Default()
    
    r.GET("/products/:id", func(c *gin.Context) {
        id, _ := strconv.Atoi(c.Param("id"))
        product, err := repo.GetProduct(id)
        if err != nil {
            c.JSON(500, gin.H{"error": err.Error()})
            return
        }
        c.JSON(200, product)
    })
    
    r.PUT("/products/:id", func(c *gin.Context) {
        id, _ := strconv.Atoi(c.Param("id"))
        // Atualiza no banco...
        
        // Invalida cache
        repo.InvalidateProduct(id)
        
        c.JSON(200, gin.H{"message": "atualizado"})
    })
    
    log.Println("Server running on :8080")
    r.Run(":8080")
}

Monitoramento e Health Checks

func healthCheck(rdb *redis.Client) map[string]interface{} {
    stats := make(map[string]interface{})
    
    // Info do servidor
    info := rdb.Info(ctx, "server")
    stats["server_info"] = info.Val()
    
    // Estatísticas de memória
    memInfo := rdb.Info(ctx, "memory")
    stats["memory"] = memInfo.Val()
    
    // Conexões ativas
    poolStats := rdb.PoolStats()
    stats["hits"] = poolStats.Hits
    stats["misses"] = poolStats.Misses
    stats["conns"] = poolStats.TotalConns
    
    return stats
}

Boas Práticas

  1. Sempre use context.Context - permite cancelamento e timeout
  2. Defina TTLs apropriados - evita acúmulo de dados obsoletos
  3. Implemente cache warming - pré-carregue dados críticos
  4. Monitore hit/miss ratio - ideal: > 80% hits
  5. Use consistent hashing - se usar múltiplos nós Redis
  6. Implemente circuit breaker - para falhas de conexão
  7. Serialize com JSON ou MessagePack - evite gob para interoperabilidade

Próximos Passos

Agora que você domina Go e Redis, explore:

FAQ

Qual a diferença entre go-redis e redigo? O go-redis é mais moderno, ativamente mantido e possui melhor suporte a novas features do Redis 7.x.

Redis é thread-safe em Go? Sim, o cliente go-redis é thread-safe e pode ser compartilhado entre múltiplas goroutines.

Quando NÃO usar Redis? Evite para dados que precisam de ACID completo, grandes objetos binários (> 512MB) ou quando durabilidade é crítica.

Como escalar Redis? Use Redis Cluster para particionamento automático ou Redis Sentinel para alta disponibilidade.


Este tutorial foi útil? Compartilhe com outros desenvolvedores Go e deixe suas dúvidas nos comentários!