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:
| Recurso | Benefício |
|---|---|
| Latência | Sub-milissegundo para operações simples |
| Throughput | 100.000+ operações por segundo |
| Estruturas | Strings, Hashes, Lists, Sets, Sorted Sets |
| Persistência | Opcional (RDB snapshots, AOF logs) |
| Pub/Sub | Mensageria em tempo real |
| Go Driver | go-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), ¬ification); 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
- Sempre use context.Context - permite cancelamento e timeout
- Defina TTLs apropriados - evita acúmulo de dados obsoletos
- Implemente cache warming - pré-carregue dados críticos
- Monitore hit/miss ratio - ideal: > 80% hits
- Use consistent hashing - se usar múltiplos nós Redis
- Implemente circuit breaker - para falhas de conexão
- Serialize com JSON ou MessagePack - evite gob para interoperabilidade
Próximos Passos
Agora que você domina Go e Redis, explore:
- Go Concurrency Patterns - Combine Redis com goroutines
- Go e gRPC - Arquiteturas de microserviços
- Go Performance - Otimize ainda mais suas aplicações
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!