Quando usar ElastiCache Redis: resolvendo lentidão no RDS com camada de cache

Você abre o painel de métricas do RDS às 14h de uma sexta-feira e vê o ReadIOPS no teto, latência de queries subindo para 800ms, e a aplicação começando a retornar timeouts. O banco está respondendo as mesmas perguntas centenas de vezes por segundo — dados de catálogo, sessões de usuário, resultados de busca que não mudam entre uma requisição e outra. Adicionar uma camada de ElastiCache Redis entre a aplicação e o RDS é a intervenção mais direta para esse padrão de problema.

TL;DR — Quando o ElastiCache Redis resolve o problema de lentidão no RDS

SituaçãoElastiCache Redis ajuda?Motivo
Mesmas queries de leitura repetidas com alta frequência✅ SimResultado servido da memória, sem tocar o RDS
Dados que mudam raramente (catálogo, configurações)✅ SimTTL longo, hit rate alto
Sessões de usuário e tokens de autenticação✅ SimEstrutura de dados nativa (Hash, String)
Queries complexas com JOINs pesados e resultado estável✅ SimEvita reprocessamento no banco
Escritas frequentes com leitura imediata do mesmo dado⚠️ CuidadoInvalidação de cache exige lógica explícita
Dados financeiros que exigem consistência forte❌ Não recomendadoCache pode servir dado desatualizado

Como o ElastiCache Redis funciona como camada de cache para o RDS

O Redis é um store de dados em memória que opera como um dicionário chave-valor de alta performance. No contexto de cache para RDS, o padrão mais comum é o cache-aside (também chamado de lazy loading): a aplicação consulta o Redis primeiro; se o dado não estiver lá (cache miss), ela busca no RDS, armazena o resultado no Redis com um TTL definido, e retorna para o cliente. Nas próximas requisições idênticas, o Redis responde diretamente.

O ElastiCache for Redis é o serviço gerenciado da AWS que provisiona, escala, faz patch e monitora clusters Redis sem que você precise operar a infraestrutura subjacente. Ele suporta dois modos de implantação relevantes para esse cenário: Cluster Mode Disabled (um shard com réplicas de leitura) e Cluster Mode Enabled (múltiplos shards para escala horizontal). Para a maioria dos casos de cache de leitura do RDS, o modo sem cluster com uma ou duas réplicas já resolve.

graph LR Client["Cliente / Aplicação"] Redis["ElastiCache Redis
(Memória)"] RDS["RDS PostgreSQL / MySQL
(Disco)"] Client -->|"1. GET product:42"| Redis Redis -->|"2a. Cache HIT retorna dado"| Client Redis -->|"2b. Cache MISS retorna nil"| Client Client -->|"3. Query SQL (apenas em MISS)"| RDS RDS -->|"4. Resultado"| Client Client -->|"5. SETEX product:42 TTL"| Redis
  1. Requisição chega na aplicação — antes de qualquer query SQL, a aplicação monta a chave de cache (ex: product:42) e consulta o Redis.
  2. Cache Hit — Redis retorna o dado serializado em memória. O RDS não é consultado. Latência típica em sub-milissegundo dentro da mesma VPC.
  3. Cache Miss — Redis retorna nil. A aplicação executa a query no RDS, recebe o resultado, grava no Redis com TTL, e retorna ao cliente.
  4. Expiração (TTL) — Após o TTL, a chave é removida automaticamente. A próxima requisição para aquela chave sofre um cache miss e repopula o cache.
  5. Invalidação explícita — Quando um dado é atualizado no RDS, a aplicação deve deletar (ou atualizar) a chave correspondente no Redis para evitar servir dados obsoletos.

Configurando o ElastiCache Redis na prática

Antes de qualquer coisa: o cluster Redis precisa estar na mesma VPC que o RDS e as instâncias da aplicação. Tráfego entre VPCs via peering funciona, mas adiciona latência e complexidade desnecessária para esse caso de uso.

1. Criar o Subnet Group

O subnet group define em quais subnets privadas o ElastiCache vai provisionar os nós. Use subnets privadas — nunca exponha o Redis à internet.

aws elasticache create-cache-subnet-group \
  --cache-subnet-group-name meu-redis-subnet-group \
  --cache-subnet-group-description 'Subnet group para cache Redis' \
  --subnet-ids subnet-0abc123456789def0 subnet-0def987654321abc0 \
  --region us-east-1

2. Criar o Security Group para o Redis

O Security Group do Redis deve aceitar conexões na porta 6379 apenas a partir do Security Group da aplicação. Isso evita que qualquer recurso na VPC acesse o cache diretamente.

aws ec2 create-security-group \
  --group-name redis-sg \
  --description 'Security Group para ElastiCache Redis' \
  --vpc-id vpc-0123456789abcdef0 \
  --region us-east-1
aws ec2 authorize-security-group-ingress \
  --group-id sg-0redis1234567890a \
  --protocol tcp \
  --port 6379 \
  --source-group sg-0app1234567890b \
  --region us-east-1

3. Criar o Cluster Redis (Cluster Mode Disabled)

Para cache de leitura do RDS, um replication group com uma réplica já oferece alta disponibilidade com failover automático. O parâmetro --automatic-failover-enabled requer pelo menos uma réplica.

aws elasticache create-replication-group \
  --replication-group-id meu-redis-rg \
  --replication-group-description 'Cache para RDS' \
  --cache-node-type cache.t4g.medium \
  --engine redis \
  --engine-version 7.0 \
  --num-cache-clusters 2 \
  --cache-subnet-group-name meu-redis-subnet-group \
  --security-group-ids sg-0redis1234567890a \
  --automatic-failover-enabled \
  --at-rest-encryption-enabled \
  --transit-encryption-enabled \
  --region us-east-1

Pricing e limites de instância variam — consulte sempre a documentação oficial da AWS para valores atualizados.

4. Implementar o padrão cache-aside na aplicação

O exemplo abaixo usa Python com a biblioteca redis-py. O mesmo padrão se aplica a qualquer linguagem.

🔽 [Clique para expandir] — Exemplo Python: cache-aside com Redis e RDS
import redis
import json
import psycopg2

# Conexão com ElastiCache Redis (use o endpoint do Primary)
redis_client = redis.Redis(
    host='meu-redis-rg.abc123.ng.0001.use1.cache.amazonaws.com',
    port=6379,
    ssl=True,  # transit-encryption-enabled exige SSL
    decode_responses=True
)

# Conexão com RDS (exemplo PostgreSQL)
def get_db_connection():
    return psycopg2.connect(
        host='meu-rds.cluster-abc123.us-east-1.rds.amazonaws.com',
        database='minha_base',
        user='app_user',
        password='senha_segura'
    )

def get_product(product_id: int) -> dict:
    cache_key = f'product:{product_id}'
    ttl_seconds = 300  # 5 minutos

    # 1. Tenta o cache primeiro
    cached = redis_client.get(cache_key)
    if cached:
        return json.loads(cached)  # Cache hit

    # 2. Cache miss — busca no RDS
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute(
        'SELECT id, name, price, category FROM products WHERE id = %s',
        (product_id,)
    )
    row = cursor.fetchone()
    cursor.close()
    conn.close()

    if row is None:
        return None

    product = {
        'id': row[0],
        'name': row[1],
        'price': float(row[2]),
        'category': row[3]
    }

    # 3. Armazena no cache com TTL
    redis_client.setex(cache_key, ttl_seconds, json.dumps(product))
    return product

def update_product(product_id: int, new_price: float):
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute(
        'UPDATE products SET price = %s WHERE id = %s',
        (new_price, product_id)
    )
    conn.commit()
    cursor.close()
    conn.close()

    # 4. Invalida o cache após atualização
    cache_key = f'product:{product_id}'
    redis_client.delete(cache_key)

IAM e permissões necessárias

O ElastiCache Redis não usa IAM para autenticação de conexão de dados por padrão — o controle de acesso é feito via Security Groups e, opcionalmente, via Redis AUTH (token de senha) ou RBAC com usuários do ElastiCache (disponível no Redis 6.x+). Para operações de gerenciamento via API/CLI, a role da aplicação precisa de permissões específicas.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ElastiCacheDescribe",
      "Effect": "Allow",
      "Action": [
        "elasticache:DescribeReplicationGroups",
        "elasticache:DescribeCacheClusters"
      ],
      "Resource": "*"
    }
  ]
}

Conexões de dados ao Redis (GET, SET, DEL) não passam pelo IAM — são controladas exclusivamente por Security Groups e autenticação no nível do Redis. Verifique a documentação do ElastiCache RBAC para configurar usuários com permissões granulares se o seu modelo de segurança exigir.

Monitorando a eficácia do cache com CloudWatch

Depois de ativar o cache, a métrica mais importante é o CacheHitRate — disponível no namespace AWS/ElastiCache no CloudWatch. Um hit rate abaixo de 80% geralmente indica TTL muito curto, chaves mal estruturadas, ou um padrão de acesso muito disperso para que o cache seja eficaz.

aws cloudwatch get-metric-statistics \
  --namespace AWS/ElastiCache \
  --metric-name CacheHitRate \
  --dimensions Name=ReplicationGroupId,Value=meu-redis-rg \
  --start-time 2024-01-15T00:00:00Z \
  --end-time 2024-01-15T23:59:59Z \
  --period 3600 \
  --statistics Average \
  --region us-east-1

Monitore também CurrConnections e Evictions. Evictions altas indicam que o nó está sem memória e removendo chaves antes do TTL — sinal de que o tipo de instância precisa ser aumentado ou que o padrão de chaves precisa ser revisado.

graph TD CW["CloudWatch AWS/ElastiCache"] M1["CacheHitRate Alvo: > 80%"] M2["Evictions Alvo: 0"] M3["CurrConnections Monitorar picos"] M4["ReplicationLag Relevante com réplicas"] M5["DatabaseMemoryUsagePercentage Alerta: > 80%"] CW --> M1 CW --> M2 CW --> M3 CW --> M4 CW --> M5 M1 -->|"Baixo hit rate"| A1["Revisar TTL ou estrutura de chaves"] M2 -->|"Evictions altas"| A2["Aumentar tipo de instância"] M5 -->|"Memória alta"| A2
  1. CacheHitRate — percentual de requisições respondidas pelo Redis sem tocar o RDS. Alvo: acima de 80% para justificar o cache.
  2. CurrConnections — conexões ativas no momento. Picos podem indicar connection leak na aplicação.
  3. Evictions — chaves removidas por falta de memória antes do TTL expirar. Zero é o ideal.
  4. ReplicationLag — atraso entre o nó primário e as réplicas. Relevante se você direcionar leituras para réplicas.
  5. DatabaseMemoryUsagePercentage — uso de memória do Redis. Acima de 80% é sinal de alerta.

O erro clássico: invalidação de cache esquecida

Aqui está o padrão de falha que aparece em produção com mais frequência nesse setup:

O time implementa o cache-aside corretamente para leituras. Hit rate sobe para 95%. Tudo parece ótimo. Três semanas depois, um cliente abre um ticket dizendo que o preço exibido no site está errado — mostra R$ 49,90 quando o correto é R$ 39,90 após uma promoção.

A investigação revela que o UPDATE no banco foi feito diretamente via script SQL de manutenção, sem passar pelo código da aplicação. O cache nunca foi invalidado. O Redis continuou servindo o preço antigo até o TTL expirar — que estava configurado para 24 horas.

A correção não é técnica, é de processo: qualquer caminho que escreva no RDS precisa também invalidar o cache correspondente. Isso inclui scripts de migração, jobs de background, e ferramentas de administração. Se isso não for possível garantir, reduza o TTL para um valor que o negócio tolere como 'dado potencialmente desatualizado'.

Cache é um contrato de consistência eventual. Você está dizendo explicitamente: 'aceito que esse dado pode estar desatualizado por até N segundos.' Se o negócio não aceita isso para um determinado dado, esse dado não deve ser cacheado.

Escolhendo o TTL certo para cada tipo de dado

Não existe um TTL universal. A escolha depende de com que frequência o dado muda no RDS e qual o impacto de servir um dado desatualizado.

Tipo de dadoTTL sugeridoJustificativa
Configurações de sistema1-6 horasMudam raramente, impacto baixo de desatualização
Catálogo de produtos (nome, descrição)15-60 minutosMudanças são planejadas com antecedência
Preços e estoque1-5 minutosMudanças frequentes, impacto alto de desatualização
Sessões de usuárioDuração da sessãoInvalidação explícita no logout
Resultados de busca30 segundos a 5 minutosDepende da frequência de atualização do índice
Dados financeiros / saldosNão cachearConsistência forte necessária

Quando o ElastiCache Redis não resolve o problema de lentidão no RDS

Cache resolve lentidão causada por leituras repetidas dos mesmos dados. Se a lentidão vem de outro lugar, adicionar Redis não vai ajudar — e pode mascarar o problema real por um tempo.

Verifique antes de investir no cache:

  • Queries sem índice — um full table scan em uma tabela de 50 milhões de linhas vai ser lento mesmo com cache, porque o cache miss ainda vai executar a query completa. Verifique o Performance Insights no RDS para identificar queries com alto db load.
  • Escritas pesadas — se o gargalo é em INSERTs/UPDATEs, cache de leitura não ajuda. Considere Read Replicas do RDS para separar carga de leitura e escrita.
  • Conexões esgotadas — se o RDS está rejeitando conexões por ter atingido max_connections, o problema é connection pooling (RDS Proxy), não cache.
  • Dados altamente dinâmicos — se cada requisição busca dados únicos (ex: relatório personalizado por usuário com filtros arbitrários), o hit rate do cache será próximo de zero e o overhead de verificar o Redis a cada requisição vai piorar a latência.

Próximos passos e documentação oficial

Com o ElastiCache Redis configurado e o padrão cache-aside implementado, os próximos pontos a endereçar são: definir alarmes no CloudWatch para Evictions e CacheHitRate, revisar o tamanho do nó com base no uso real de memória após a primeira semana de produção, e avaliar se o RDS Proxy complementa a solução para gerenciar connection pooling.

Glossário

TermoDefinição
Cache-aside (Lazy Loading)Padrão onde a aplicação verifica o cache antes do banco. Em caso de miss, busca no banco e popula o cache.
TTL (Time to Live)Tempo em segundos após o qual uma chave no Redis é automaticamente removida.
Cache Hit / Cache MissHit: dado encontrado no cache. Miss: dado não encontrado, necessário buscar na fonte primária.
EvictionRemoção de chaves pelo Redis quando a memória disponível é insuficiente, antes do TTL expirar.
Replication GroupConjunto de nós Redis no ElastiCache composto por um primário e uma ou mais réplicas de leitura.

Comentários

Postagens mais visitadas deste blog

Variáveis de Ambiente no Lambda: Configuração, Acesso e Criptografia com KMS

Monitoramento de Memória RAM no EC2: Por que o CloudWatch Agent é Obrigatório

S3 Access Denied: Por que 'Bloquear Acesso Público' impede seu objeto mesmo após torná-lo público