<h2>Entendendo Redis e a Biblioteca go-redis</h2>
<p>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.</p>
<p>A biblioteca <code>go-redis</code> é 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.</p>
<h2>Instalação e Configuração Básica</h2>
<h3>Preparando o Ambiente</h3>
<p>Primeiro, você precisa ter Redis instalado e rodando. No Linux, use <code>sudo apt-get install redis-server</code>. No macOS, utilize <code>brew install redis</code>. Você pode verificar se está funcionando com <code>redis-cli ping</code>, que deve retornar <code>PONG</code>.</p>
<p>Em seu projeto Go, instale a biblioteca go-redis:</p>
<pre><code class="language-bash">go get github.com/redis/go-redis/v9</code></pre>
<h3>Conectando ao Redis</h3>
<p>A conexão é o primeiro passo. Você cria um cliente que mantém um pool de conexões interno e o reutiliza. O contexto (<code>context.Context</code>) é fundamental em Go para controle de timeouts e cancelamentos:</p>
<pre><code class="language-go">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
}</code></pre>
<p>Observe que criamos um contexto com <code>context.Background()</code>, que é usado como base. Em aplicações reais, você passará contextos com timeouts para operações específicas. A função <code>defer client.Close()</code> garante que a conexão seja fechada quando a função terminar.</p>
<h2>Cache de Dados com Redis</h2>
<h3>O Conceito de Cache</h3>
<p>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.</p>
<h3>Implementando um Cache Simples</h3>
<p>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:</p>
<pre><code class="language-go">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))
}</code></pre>
<p>Neste exemplo, a primeira chamada demora ~2 segundos (simulação do banco). A segunda chamada retorna em milissegundos do cache. Usamos <code>Set(ctx, key, value, expiration)</code> para armazenar com um TTL (time-to-live), garantindo que dados antigos sejam removidos automaticamente.</p>
<h3>Cache com Padrão Cache-Aside Avançado</h3>
<p>Em sistemas complexos, você pode precisar invalidar o cache quando dados são atualizados. Veja um exemplo com operação de atualização:</p>
<pre><code class="language-go">// 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
}</code></pre>
<h2>Pub/Sub: Comunicação em Tempo Real</h2>
<h3>Entendendo o Padrão Publish/Subscribe</h3>
<p>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.</p>
<h3>Implementando Publisher e Subscriber</h3>
<p>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:</p>
<pre><code class="language-go">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)
}</code></pre>
<p>Execute este código em dois terminais ou em goroutines. Os subscribers precisam estar escutando <em>antes</em> das mensagens serem publicadas. Quando você executa, verá que múltiplos subscribers recebem a mesma mensagem simultaneamente—esse é o poder do Pub/Sub.</p>
<h3>Padrão Subscribe com Pattern</h3>
<p>Redis também permite inscrever-se em padrões. Por exemplo, <code>orders:*</code> capturaria todas as mensagens de canais começando com "orders:":</p>
<pre><code class="language-go">// 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)
}</code></pre>
<h2>Filas de Processamento Assíncrono</h2>
<h3>Por Que Usar Filas?</h3>
<p>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 <em>persistem</em> mensagens até que sejam consumidas. Você pode ter múltiplos workers processando a mesma fila, escalando facilmente.</p>
<h3>Implementando Fila com LPUSH e RPOP</h3>
<p>Redis oferece operações de lista para implementar filas. <code>LPUSH</code> adiciona à esquerda, <code>RPOP</code> remove da direita (FIFO):</p>
<pre><code class="language-go">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)
}</code></pre>
<p>Neste exemplo, <code>BRPop</code> (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.</p>
<h3>Fila Robusta com Retry</h3>
<p>Em produção, tarefas podem falhar. Uma abordagem é usar uma fila de "dead letter" para tarefas que falharam múltiplas vezes:</p>
<pre><code class="language-go">// 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
}</code></pre>
<h2>Estratégias de Otimização e Boas Práticas</h2>
<h3>Pipelining para Múltiplas Operações</h3>
<p>Quando você precisa fazer várias operações, pipelinning reduz latência agrupando-as em uma única chamada de rede:</p>
<pre><code class="language-go">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
}</code></pre>
<h3>Monitorando Conexões</h3>
<p>Em aplicações que lidam com muito tráfego, monitore a saúde da conexão:</p>
<pre><code class="language-go">// 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())
}
}()</code></pre>
<h3>Evitar Chaves Grandes</h3>
<p>Armazenar objetos muito grandes em uma única chave degrada performance. Prefira normalizar—guarde referências em uma estrutura de índice:</p>
<pre><code class="language-go">// 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)</code></pre>
<h2>Conclusão</h2>
<p>Durante este artigo, cobrimos três pilares da integração Redis com Go. <strong>Primeiro</strong>, o cache com <code>go-redis</code> reduz drasticamente latência ao armazenar dados acessados frequentemente, com suporte automático a TTL para evitar dados obsoletos. <strong>Segundo</strong>, Pub/Sub permite comunicação em tempo real entre componentes de um sistema, escalando para múltiplos subscribers sem overhead. <strong>Terceiro</strong>, filas implementadas com operações de lista (LPUSH/RPOP ou BRPOP) desacoplam produtores e consumidores, permitindo processamento assíncrono e escalável de tarefas.</p>
<p>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.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://redis.io/docs/clients/go/" target="_blank" rel="noopener noreferrer">Documentação Oficial go-redis</a></li>
<li><a href="https://redis.io/docs/data-types/" target="_blank" rel="noopener noreferrer">Redis Documentation - Data Types</a></li>
<li><a href="https://redis.io/docs/interact/pubsub/" target="_blank" rel="noopener noreferrer">Redis Pub/Sub Pattern</a></li>
<li><a href="https://golang.org/doc/effective_go" target="_blank" rel="noopener noreferrer">Effective Go - Context Package</a></li>
<li><a href="https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/BestPractices.html" target="_blank" rel="noopener noreferrer">Redis Best Practices - AWS</a></li>
</ul>
<p><!-- FIM --></p>