Go

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

16 min de leitura

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

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

<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 (

&quot;context&quot;

&quot;fmt&quot;

&quot;github.com/redis/go-redis/v9&quot;

)

func main() {

// Conectar ao Redis rodando em localhost:6379

client := redis.NewClient(&amp;redis.Options{

Addr: &quot;localhost:6379&quot;,

})

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 (&quot;cache hit&quot;), retorna imediatamente. Se não estiver (&quot;cache miss&quot;), 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 (

&quot;context&quot;

&quot;encoding/json&quot;

&quot;fmt&quot;

&quot;github.com/redis/go-redis/v9&quot;

&quot;time&quot;

)

type User struct {

ID int json:&quot;id&quot;

Name string json:&quot;name&quot;

Email string json:&quot;email&quot;

}

// Simula uma busca custosa em banco de dados

func fetchUserFromDB(userID int) *User {

time.Sleep(2 * time.Second) // Simula latência

return &amp;User{

ID: userID,

Name: &quot;João Silva&quot;,

Email: &quot;joao@example.com&quot;,

}

}

// GetUser implementa cache com fallback

func GetUser(client redis.Client, ctx context.Context, userID int) (User, error) {

key := fmt.Sprintf(&quot;user:%d&quot;, userID)

// Tenta recuperar do cache

cachedData, err := client.Get(ctx, key).Result()

if err == nil {

// Cache hit

var user User

json.Unmarshal([]byte(cachedData), &amp;user)

fmt.Println(&quot;Recuperado do cache&quot;)

return &amp;user, nil

}

// Cache miss: busca do banco

fmt.Println(&quot;Recuperado do banco de dados&quot;)

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(&amp;redis.Options{

Addr: &quot;localhost:6379&quot;,

})

defer client.Close()

ctx := context.Background()

// Primeira chamada: vai para o banco

start := time.Now()

user1, _ := GetUser(client, ctx, 1)

fmt.Printf(&quot;User: %v | Tempo: %v\n\n&quot;, user1.Name, time.Since(start))

// Segunda chamada: vem do cache (muito mais rápida)

start = time.Now()

user2, _ := GetUser(client, ctx, 1)

fmt.Printf(&quot;User: %v | Tempo: %v\n&quot;, 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(&quot;Usuário atualizado no banco&quot;)

// Invalida o cache

key := fmt.Sprintf(&quot;user:%d&quot;, user.ID)

err := client.Del(ctx, key).Err()

if err != nil {

return err

}

fmt.Println(&quot;Cache invalidado&quot;)

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 (

&quot;context&quot;

&quot;fmt&quot;

&quot;github.com/redis/go-redis/v9&quot;

&quot;time&quot;

)

// Publisher envia mensagens para um canal

func PublishOrderNotification(client *redis.Client, ctx context.Context) {

channel := &quot;orders:new&quot;

messages := []string{

&quot;Pedido #1001 criado&quot;,

&quot;Pedido #1002 criado&quot;,

&quot;Pedido #1003 criado&quot;,

}

for i, msg := range messages {

// Publica a mensagem

err := client.Publish(ctx, channel, msg).Err()

if err != nil {

fmt.Printf(&quot;Erro ao publicar: %v\n&quot;, err)

}

fmt.Printf(&quot;[PUB] %s\n&quot;, msg)

time.Sleep(1 * time.Second)

}

}

// Subscriber escuta mensagens de um canal

func SubscribeOrderNotifications(client *redis.Client, ctx context.Context, name string) {

channel := &quot;orders:new&quot;

sub := client.Subscribe(ctx, channel)

defer sub.Close()

// Cria um canal Go para receber mensagens

ch := sub.Channel()

fmt.Printf(&quot;[%s] Escutando no canal &#039;%s&#039;...\n&quot;, name, channel)

for i := 0; i &lt; 3; i++ {

msg := &lt;-ch

fmt.Printf(&quot;[%s] Recebido: %s\n&quot;, name, msg.Payload)

}

}

func main() {

client := redis.NewClient(&amp;redis.Options{

Addr: &quot;localhost:6379&quot;,

})

defer client.Close()

ctx := context.Background()

// Inicia subscribers em goroutines

go SubscribeOrderNotifications(client, ctx, &quot;Subscriber-1&quot;)

go SubscribeOrderNotifications(client, ctx, &quot;Subscriber-2&quot;)

// 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 &quot;orders:&quot;:</p>

<pre><code class="language-go">// Subscrever com padrão

pSub := client.PSubscribe(ctx, &quot;orders:&quot;, &quot;notifications:&quot;)

defer pSub.Close()

ch := pSub.Channel()

for msg := range ch {

fmt.Printf(&quot;Canal: %s, Mensagem: %s\n&quot;, 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 (

&quot;context&quot;

&quot;encoding/json&quot;

&quot;fmt&quot;

&quot;github.com/redis/go-redis/v9&quot;

&quot;time&quot;

)

type Task struct {

ID int json:&quot;id&quot;

Name string json:&quot;name&quot;

Data string json:&quot;data&quot;

}

// EnqueueTask adiciona uma tarefa à fila

func EnqueueTask(client redis.Client, ctx context.Context, task Task) error {

queueKey := &quot;tasks:queue&quot;

taskJSON, _ := json.Marshal(task)

// Adiciona à esquerda (LPUSH)

err := client.LPush(ctx, queueKey, taskJSON).Err()

if err != nil {

return err

}

fmt.Printf(&quot;Tarefa enfileirada: ID=%d, Nome=%s\n&quot;, task.ID, task.Name)

return nil

}

// Worker processa tarefas da fila

func Worker(client *redis.Client, ctx context.Context, workerID int) {

queueKey := &quot;tasks:queue&quot;

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(&quot;[Worker-%d] Nenhuma tarefa, aguardando...\n&quot;, workerID)

continue

}

fmt.Printf(&quot;[Worker-%d] Erro: %v\n&quot;, workerID, err)

break

}

// Decodifica a tarefa

var task Task

json.Unmarshal([]byte(result[1]), &amp;task)

// Processa

fmt.Printf(&quot;[Worker-%d] Processando: ID=%d, Nome=%s\n&quot;, workerID, task.ID, task.Name)

time.Sleep(1 * time.Second) // Simula processamento

fmt.Printf(&quot;[Worker-%d] Tarefa concluída: ID=%d\n&quot;, workerID, task.ID)

}

}

func main() {

client := redis.NewClient(&amp;redis.Options{

Addr: &quot;localhost:6379&quot;,

})

defer client.Close()

ctx := context.Background()

// Limpa a fila (opcional)

client.Del(ctx, &quot;tasks:queue&quot;)

// 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 &lt;= 5; i++ {

task := &amp;Task{

ID: i,

Name: fmt.Sprintf(&quot;Tarefa %d&quot;, i),

Data: &quot;dados importantes&quot;,

}

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 &quot;dead letter&quot; 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(&quot;task:retry:%d&quot;, task.ID)

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

// Tenta processar

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

if err != nil {

if retries &lt; maxRetries {

// Re-enfileira

retries++

client.Set(ctx, retryKey, retries, 24*time.Hour)

EnqueueTask(client, ctx, task)

fmt.Printf(&quot;Tarefa %d re-enfileirada (tentativa %d)\n&quot;, task.ID, retries)

} else {

// Envia para dead letter queue

deadLetterKey := &quot;tasks:dead_letter&quot;

taskJSON, _ := json.Marshal(task)

client.LPush(ctx, deadLetterKey, taskJSON)

fmt.Printf(&quot;Tarefa %d descartada (dead letter)\n&quot;, 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(&quot;user:%d&quot;, 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(&quot;Redis indisponível: %v\n&quot;, err)

return err

}

fmt.Println(&quot;Redis OK&quot;)

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, &quot;all:users&quot;, largeJSON, time.Hour)

// Bom: usar Set ou Hash

client.SAdd(ctx, &quot;users:ids&quot;, &quot;1&quot;, &quot;2&quot;, &quot;3&quot;)

// E armazenar cada usuário separadamente

client.Set(ctx, &quot;user:1&quot;, 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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

Guia Completo de Context em Go: Cancelamento, Timeout e Propagação de Valores
Guia Completo de Context em Go: Cancelamento, Timeout e Propagação de Valores

Context em Go: Cancelamento, Timeout e Propagação de Valores O é um dos pacot...

Boas Práticas de CQRS e Event Sourcing em Go: Implementação Prática para Times Ágeis
Boas Práticas de CQRS e Event Sourcing em Go: Implementação Prática para Times Ágeis

Entendendo CQRS: O Padrão de Separação de Responsabilidades CQRS significa Co...

O que Todo Dev Deve Saber sobre Embedding em Go: Composição de Structs e Interfaces
O que Todo Dev Deve Saber sobre Embedding em Go: Composição de Structs e Interfaces

Embedding em Go: O que é e Por Que Importa Embedding é um mecanismo poderoso...