PostgreSQL vs Redis: Como Substituir Cache, Filas e Pub/Sub Usando Apenas o Banco de Dados e Reduzir Custos em Startups

Published on: 2026-04-05
Post image
pt postgresql-vs-redis substituir-redis-por-postgresql cache-com-postgresql filas-com-postgresql-skip-locked pub-sub-postgresql-listen-notify postgresql-como-alternativa-ao-redis usar-apenas-postgresql-na-arquitetura reduzir-custos-com-infraestrutura

Este artigo apresenta como recursos atuais do PostgreSQL tornam opcional o uso do Redis na maioria dos cenários comuns em startups. A proposta é explicar, de forma didática, como o banco relacional consegue cobrir cache, pub/sub, filas de trabalho, sessões e rate limiting sem adicionar um segundo serviço. O foco está em recursos nativos como tabelas UNLOGGED, LISTEN/NOTIFY, SKIP LOCKED e o ecossistema de JSONB com JSON_TABLE. A abordagem considera princípios de desempenho, desenho de dados e operação cotidiana. O objetivo é permitir decisões conscientes, comparando custos, limitações e boas práticas.

O conteúdo parte do zero e define cada termo técnico no momento em que aparece. Cada seção traz explicações curtas e progressivas, exemplos práticos e pontos de atenção. Quando necessário, são usados blocos de código reais seguindo o padrão indicado, com comentários e identificadores em português. As listas destacam decisões arquiteturais e critérios objetivos para adoção de cada solução. O material é autossuficiente e cobre começo, meio e fim do tema proposto.

O que normalmente o Redis resolve em stacks de startups

O Redis é um armazenamento em memória voltado para desempenho, muito adotado em aplicações web. Na prática, sua utilização mais frequente concentra-se em poucos casos de uso recorrentes. Esses casos cobrem funcionalidades que priorizam latência muito baixa e estruturas de dados simples. A seguir, apresenta-se uma lista com os usos mais comuns observados em ambientes de produto. O objetivo é situar o problema antes de mapear soluções equivalentes com PostgreSQL.

Os usos recorrentes incluem cache de respostas, pub/sub, filas, sessões e limitação de taxa. Cada item representa uma necessidade transversal encontrada em APIs, sistemas de filas e interfaces em tempo quase real. A seleção não cobre cenários extremos, como comandos geoespaciais avançados ou estruturas fortemente especializadas. Em seguida, a lista mostra os cinco usos mais populares e amplamente documentados. A lista serve de referência para as seções seguintes.

  • Cache de respostas de API e resultados de consultas custosas.
  • Pub/sub para notificações e atualizações em tempo real.
  • Filas de trabalho em segundo plano.
  • Armazenamento de sessões de usuários.
  • Rate limiting com janelas deslizantes.

Por que o PostgreSQL cobre esses mesmos casos com segurança

O PostgreSQL evoluiu em recursos que reduzem a necessidade de serviços adicionais. As tabelas UNLOGGED oferecem escrita rápida sem durabilidade, ideais para cache. O LISTEN/NOTIFY fornece um canal pub/sub leve, integrado à própria conexão do banco. O SKIP LOCKED resolve concorrência em filas com segurança transacional e sem orquestradores externos. O suporte a JSONB e a função JSON_TABLE ampliam a flexibilidade de dados sem abandonar o SQL.

Quando essas capacidades são combinadas, grande parte das necessidades de startups é satisfeita com um único serviço de dados. Isso reduz latência de rede, simplifica a observabilidade e encolhe a fatura de infraestrutura. A centralização também reduz ambiguidade operacional sobre onde está cada informação. Tudo isso preserva atomicidade, consistência e isolamento, fundamentos de bancos transacionais. As próximas seções detalham como aplicar cada recurso com exemplos práticos.

Cache rápido com tabelas UNLOGGED

Tabelas UNLOGGED não gravam no WAL (Write-Ahead Log), reduzindo a sobrecarga de escrita e tornando-as ideais para dados efêmeros. Esse comportamento implica perda de dados em caso de falha, o que é desejável em um cache. A ausência de WAL também evita replicação lógica do conteúdo, reforçando a ideia de temporariedade. Índices continuam disponíveis e úteis para chave e expiração. A seguir, apresenta-se um esquema completo de cache com TTL, upsert e limpeza programada.

O exemplo inclui criação de tabela, escrita com validade e leitura condicionada à expiração. Também é mostrada uma rotina de limpeza periódica simples. Na prática, a limpeza pode ser agendada via extensões de agendamento ou tarefas externas. Para cargas maiores, partições por data podem facilitar a remoção de dados expirados. O código SQL ilustra o fluxo básico pronto para produção.

-- Tabela UNLOGGED para cache efêmero
CREATE UNLOGGED TABLE IF NOT EXISTS cache_store (
  chave        text PRIMARY KEY,
  valor        jsonb NOT NULL,
  expira_em    timestamptz NOT NULL DEFAULT now() + interval '30 minutes',
  criado_em    timestamptz NOT NULL DEFAULT now()
);

-- Índices úteis para TTL e acesso por chave
CREATE INDEX IF NOT EXISTS idx_cache_store_expira_em ON cache_store (expira_em);
CREATE INDEX IF NOT EXISTS idx_cache_store_criado_em ON cache_store (criado_em);

-- Escrever ou atualizar com TTL (upsert)
INSERT INTO cache_store (chave, valor, expira_em)
VALUES ('usuario:123:perfil', '{"nome":"Ana","plano":"pro"}', now() + interval '20 minutes')
ON CONFLICT (chave) DO UPDATE
  SET valor = EXCLUDED.valor,
      expira_em = EXCLUDED.expira_em;

-- Ler respeitando a expiração
SELECT valor
FROM cache_store
WHERE chave = 'usuario:123:perfil'
  AND expira_em > now();

-- Limpeza periódica de itens expirados
DELETE FROM cache_store
WHERE expira_em <= now();

Para facilitar a operação, uma tarefa agendada executa a limpeza em intervalos curtos. Quando disponível, uma extensão de agendamento no banco simplifica o processo. Abaixo, um exemplo de agendamento direto no banco, com intervalo a cada cinco minutos. Alternativamente, um cron do sistema pode executar o comando via psql. O essencial é manter a tabela pequena e quente para boas latências.

-- Exemplo de agendamento interno (quando extensão de cron estiver disponível)
SELECT cron.schedule(
  'limpa_cache_expirado_cada_5m',
  '*/5 * * * *',
  $$DELETE FROM cache_store WHERE expira_em <= now();$$
);

Pub/sub com LISTEN/NOTIFY para eventos em tempo real

O recurso LISTEN/NOTIFY fornece um canal de notificação nativo no PostgreSQL. O comando NOTIFY publica uma mensagem em um canal nomeado e as conexões que executaram LISTEN nesse canal recebem o evento. O payload comporta até 8 KB, suficiente para envelopes de evento com identificadores e estado. Para dados maiores, recomenda-se enviar apenas o identificador e buscar o registro completo depois. A seguir, apresenta-se um exemplo completo de publicação e consumo.

O primeiro trecho demonstra a publicação do evento diretamente em SQL. Em seguida, um consumidor em Python mostra como assinar e aguardar notificações. O processamento ocorre em uma conexão que permanece escutando e reage a cada payload. Esse padrão evita perdas comuns de mensagens quando o dado fonte está no mesmo banco. O exemplo adota nomes de variáveis e comentários em português, mantendo clareza.

-- Publicar uma notificação com um envelope enxuto
SELECT pg_notify(
  'atualizacoes_pedidos',
  json_build_object(
    'pedido_id', 9812,
    'status', 'enviado',
    'usuario_id', 441
  )::text
);
# Consumidor de notificações com psycopg
import os
import select
import psycopg

def abrir_conexao():
    return psycopg.connect(os.environ["DATABASE_URL"])

with abrir_conexao() as conexao:
    with conexao.cursor() as cur:
        cur.execute("LISTEN atualizacoes_pedidos;")
        print("Assinado no canal: atualizacoes_pedidos")

        while True:
            # Espera por atividade na conexão
            if select.select([conexao], [], [], 30) == ([], [], []):
                continue  # timeout de espera
            conexao.poll()
            while conexao.notifies:
                notificacao = conexao.notifies.pop(0)
                print("Notificação recebida:", notificacao.payload)
                # Buscar detalhes se necessário:
                # SELECT ... FROM pedidos WHERE id = <pedido_id>

Esse mecanismo é leve e evita um salto de rede adicional quando o dado alvo já está no banco. Em ambientes com poolers em modo transacional, é importante garantir sessões dedicadas para LISTEN. Para escala elevada, canais podem ser particionados por entidade, como um canal por organização. Requisições idempotentes ajudam a tolerar reconexões e repetições. A observabilidade pode registrar contagem e atraso de entrega por canal.

Filas confiáveis com SKIP LOCKED e transações

O SKIP LOCKED permite que vários trabalhadores consultem uma fila concorrente sem colisão. A cláusula ignora linhas já bloqueadas por outra transação, garantindo que cada trabalho seja reclamado por um único consumidor. O padrão de implementação usa um SELECT FOR UPDATE SKIP LOCKED seguido de UPDATE para marcar o status. Campos de tentativa e horário de próxima execução viabilizam retentativas e backoff. A seguir, apresenta-se um esquema de fila pronto para produção.

O exemplo mostra a criação da tabela, a consulta de reivindicação e um trabalhador em Python. Também são incluídos campos para reagendamento e erros, úteis na análise posterior. Esse modelo simplifica a operação por concentrar estado e histórico em um só lugar. Com índices adequados, a fila sustenta volumes expressivos sem coordenação externa. O código a seguir demonstra o fluxo completo.

-- Estrutura de fila de trabalhos
CREATE TABLE IF NOT EXISTS fila_trabalhos (
  id             bigserial PRIMARY KEY,
  tipo           text NOT NULL,
  carga          jsonb NOT NULL,
  status         text NOT NULL DEFAULT 'pendente', -- pendente, processando, concluido, falhou
  criado_em      timestamptz NOT NULL DEFAULT now(),
  proxima_exec   timestamptz NOT NULL DEFAULT now(),
  tentativas     int NOT NULL DEFAULT 0,
  erro_ultima    text
);

CREATE INDEX IF NOT EXISTS idx_fila_status_proxima
  ON fila_trabalhos (status, proxima_exec, criado_em);
-- Reivindicar um trabalho pronto para processamento
WITH reivindicado AS (
  SELECT id
  FROM fila_trabalhos
  WHERE status = 'pendente'
    AND proxima_exec <= now()
  ORDER BY criado_em
  LIMIT 1
  FOR UPDATE SKIP LOCKED
)
UPDATE fila_trabalhos f
SET status = 'processando'
FROM reivindicado r
WHERE f.id = r.id
RETURNING f.*;
# Trabalhador simples com retentativa e backoff exponencial
import os
import time
import psycopg

def processar_trabalho(trabalho):
    # Processamento específico por tipo
    if trabalho["tipo"] == "enviar_email":
        # ... enviar e-mail ...
        return True, None
    return True, None

def proxima_tentativa(tentativas):
    atraso = min(60, 2 ** tentativas)  # segundos, limite 60
    return atraso

with psycopg.connect(os.environ["DATABASE_URL"]) as conexao:
    conexao.autocommit = False
    while True:
        try:
            with conexao.cursor() as cur:
                cur.execute("""
                    WITH reivindicado AS (
                      SELECT id
                      FROM fila_trabalhos
                      WHERE status = 'pendente' AND proxima_exec <= now()
                      ORDER BY criado_em
                      LIMIT 1
                      FOR UPDATE SKIP LOCKED
                    )
                    UPDATE fila_trabalhos f
                    SET status = 'processando'
                    FROM reivindicado r
                    WHERE f.id = r.id
                    RETURNING f.id, f.tipo, f.carga, f.tentativas;
                """)
                linha = cur.fetchone()
                if not linha:
                    conexao.commit()
                    time.sleep(0.2)
                    continue

                trabalho = {"id": linha[0], "tipo": linha[1], "carga": linha[2], "tentativas": linha[3]}
                sucesso, erro = processar_trabalho(trabalho)

                if sucesso:
                    cur.execute("UPDATE fila_trabalhos SET status='concluido' WHERE id=%s", (trabalho["id"],))
                else:
                    tent = trabalho["tentativas"] + 1
                    atraso = proxima_tentativa(tent)
                    cur.execute("""
                        UPDATE fila_trabalhos
                        SET status='pendente',
                            tentativas=%s,
                            erro_ultima=%s,
                            proxima_exec=now() + make_interval(secs => %s)
                        WHERE id=%s
                    """, (tent, erro, atraso, trabalho["id"]))
                conexao.commit()
        except Exception as exc:
            conexao.rollback()
            time.sleep(1)

Esse desenho elimina condições de corrida comuns em filas baseadas apenas em updates sem bloqueio. As transações garantem atomicidade entre reivindicação e mudança de status. Métricas de idade dos pendentes, tempo em processamento e taxa de falhas orientam capacidade de consumidores. Em cenários com grande volume, particionamento por tipo ou data pode ajudar. Logs de erro mantidos na própria tabela aceleram a depuração.

Armazenamento de sessões com TTL e upsert

Sessões são dados transitórios que combinam chaves curtas, prazos de validade e leituras frequentes. Em PostgreSQL, uma tabela simples com índice por chave e expiração resolve o problema. O uso de UNLOGGED acelera escrita caso a durabilidade seja dispensável. Alternativamente, tabelas regulares oferecem recuperação após falhas para sessões mais críticas. A seguir, apresenta-se um esquema de sessões com TTL e um exemplo básico de acesso em JavaScript.

O fluxo inclui criação, leitura, atualização de validade e limpeza. Em aplicações web, o TTL costuma ser renovado a cada acesso, mantendo a sessão ativa. Uma tarefa periódica remove chaves expiradas para manter a tabela enxuta. Para ambientes distribuídos, o mesmo banco atende instâncias múltiplas sem coordenação extra. O exemplo ilustra o ciclo completo.

-- Tabela de sessões (usar UNLOGGED se a perda em crash for aceitável)
CREATE TABLE IF NOT EXISTS sessoes (
  id            text PRIMARY KEY,
  dados         jsonb NOT NULL,
  expira_em     timestamptz NOT NULL,
  atualizado_em timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_sessoes_expira_em ON sessoes (expira_em);
// Exemplo Node.js (pg) para ler e renovar sessão
import pg from "pg";
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });

export async function obterSessao(idSessao) {
  const cliente = await pool.connect();
  try {
    const { rows } = await cliente.query(
      "SELECT dados FROM sessoes WHERE id=$1 AND expira_em > now()",
      [idSessao]
    );
    if (rows.length === 0) return null;

    // Renovar TTL por atividade
    await cliente.query(
      "UPDATE sessoes SET expira_em = now() + interval '30 minutes', atualizado_em = now() WHERE id=$1",
      [idSessao]
    );
    return rows[0].dados;
  } finally {
    cliente.release();
  }
}

export async function salvarSessao(idSessao, dados) {
  const cliente = await pool.connect();
  try {
    await cliente.query(
      `INSERT INTO sessoes (id, dados, expira_em)
       VALUES ($1, $2, now() + interval '30 minutes')
       ON CONFLICT (id) DO UPDATE
         SET dados = EXCLUDED.dados,
             expira_em = EXCLUDED.expira_em,
             atualizado_em = now()`,
      [idSessao, dados]
    );
  } finally {
    cliente.release();
  }
}

Esse padrão simplifica a lógica da aplicação e usa o banco já existente como fonte de verdade. O índice por expiração sustenta a limpeza eficiente. Para segurança, convém criptografar ou assinar dados sensíveis no próprio payload. Em sessões com tokens JWT, a tabela pode armazenar apenas uma lista de revogação. A governança de dados permanece centralizada.

Rate limiting com janela deslizante em SQL

Limitação de taxa controla quantas operações uma entidade pode executar num intervalo. Um desenho clássico registra tentativas e contabiliza eventos recentes numa janela móvel. Em PostgreSQL, uma combinação de upsert e contagem por intervalo atende boa parte dos casos. Para latências baixas, uma tabela UNLOGGED reduz custo de escrita. A seguir, apresenta-se um esquema simples de contagem com janela de 1 minuto.

O exemplo registra tentativas por chave e usa uma consulta para decidir se uma nova ação é permitida. Um índice por chave acelera leituras e exclusões periódicas. Em cenários de alto volume, somários por minuto reduzem cardinalidade e aumentam eficiência. A precisão continua aceitável para proteções de API. O SQL abaixo ilustra o fluxo.

-- Tabela de eventos para rate limiting (UNLOGGED recomendado)
CREATE UNLOGGED TABLE IF NOT EXISTS limite_eventos (
  chave       text NOT NULL,
  ocorrido_em timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS idx_limite_chave_tempo
  ON limite_eventos (chave, ocorrido_em);

-- Checar o total na janela deslizante (ex.: 100 req por minuto)
WITH recentes AS (
  SELECT count(*) AS total
  FROM limite_eventos
  WHERE chave = 'ip:203.0.113.1'
    AND ocorrido_em > now() - interval '60 seconds'
)
SELECT (total < 100) AS permitido FROM recentes;

-- Registrar tentativa se permitido
INSERT INTO limite_eventos (chave) VALUES ('ip:203.0.113.1');

-- Limpeza periódica de registros antigos
DELETE FROM limite_eventos
WHERE ocorrido_em <= now() - interval '5 minutes';

Para precisão com menor custo, um balde por minuto com upsert reduz linhas. Esse modelo aproxima a janela deslizante usando o minuto corrente e o anterior ponderados. Em muitas APIs, a aproximação é suficiente e consome menos I/O. A seguir, um exemplo de agregação por minuto. O padrão equilibra eficiência e simplicidade.

-- Contadores agregados por minuto
CREATE UNLOGGED TABLE IF NOT EXISTS limite_balde_minuto (
  chave     text NOT NULL,
  minuto    timestamptz NOT NULL,
  contagem  int NOT NULL DEFAULT 0,
  PRIMARY KEY (chave, minuto)
);

-- Incremento atômico
INSERT INTO limite_balde_minuto (chave, minuto, contagem)
VALUES ('ip:203.0.113.1', date_trunc('minute', now()), 1)
ON CONFLICT (chave, minuto)
DO UPDATE SET contagem = limite_balde_minuto.contagem + 1;

-- Consulta do total aproximado da janela de 60s
WITH b AS (
  SELECT minuto, contagem
  FROM limite_balde_minuto
  WHERE chave = 'ip:203.0.113.1'
    AND minuto >= date_trunc('minute', now()) - interval '1 minute'
)
SELECT coalesce(sum(contagem), 0) AS total_aproximado FROM b;

JSONB e JSON_TABLE no PostgreSQL 17

O tipo JSONB armazena documentos JSON em formato binário indexável. Essa capacidade permite flexibilidade de esquema com possibilidade de índices parciais e funcionais. A função JSON_TABLE, disponível a partir do PostgreSQL 17, projeta estruturas aninhadas em linhas relacionais durante a consulta. Essa projeção simplifica filtros e junções sobre dados semiestruturados. A seguir, apresenta-se um exemplo prático de consulta sobre um array JSON.

O primeiro trecho mostra como transformar pedidos recentes em linhas consultáveis. Em seguida, um exemplo de índice funcional acelera filtros por chaves de alto uso. Para versões anteriores, funções como jsonb_path_query oferecem caminhos alternativos. Em aplicações com documentos complexos, a estratégia mista reduz migrações de esquema. O SQL abaixo ilustra a consulta com JSON_TABLE.

-- Tabela de usuários com pedidos recentes em JSONB
CREATE TABLE IF NOT EXISTS usuarios (
  id            bigserial PRIMARY KEY,
  nome          text NOT NULL,
  pedidos_json  jsonb NOT NULL DEFAULT '{"pedidos":[]}'::jsonb
);

-- Consulta JSON_TABLE: projeta cada pedido como linha
SELECT u.id AS usuario_id, p.*
FROM usuarios u,
JSON_TABLE(
  u.pedidos_json,
  '$.pedidos[*]'
  COLUMNS (
    pedido_id  int   PATH '$.id',
    total      numeric PATH '$.total',
    status     text  PATH '$.status'
  )
) AS p
WHERE p.status = 'pendente';
-- Índice funcional para acelerar buscas por status dentro do JSONB
CREATE INDEX IF NOT EXISTS idx_usuarios_status_json
  ON usuarios ((pedidos_json #> '{pedidos}'));

-- Exemplo alternativo (versões < 17): extrair itens com jsonb_path_query
SELECT u.id, p
FROM usuarios u
CROSS JOIN LATERAL jsonb_path_query(u.pedidos_json, '$.pedidos[*] ? (@.status == "pendente")') AS p;

Esse conjunto de recursos reduz o apelo de armazenamentos chave-valor para flexibilidade de esquema. Ao mesmo tempo, mantém fortes garantias transacionais e o poder do SQL. A indexação correta evita varreduras custosas em documentos grandes. Para auditoria, os dados podem coexistir em colunas estruturadas e JSONB. O equilíbrio entre estrutura e flexibilidade melhora a evolução do modelo.

Custo e complexidade operacional ao manter dois serviços

A execução de um segundo serviço adiciona custo mensal direto e indireto. Além da cobrança do provedor, surge a necessidade de monitorar, fazer backup e escalar separadamente. Cada salto de rede entre aplicação e dados aumenta a latência de ponta a ponta. Equipes pequenas sentem mais o peso cognitivo de operar múltiplas peças. A seguir, apresenta-se uma lista de impactos operacionais típicos.

  • Dois planos de observabilidade: métricas, logs e alertas duplicados.
  • Rotinas de backup, restauração e testes de DR distintas.
  • Políticas de segurança e credenciais adicionais para gerir.
  • Latência extra por hop de rede em acessos encadeados.
  • Runbook e on-call mais longos, aumentando MTTR.

Concentrar cache, filas e sessões no PostgreSQL remove um salto de rede em requisições que tocam dados relacionais e efêmeros. Essa eliminação pode superar diferenças pequenas de latência por operação de cache. A consolidação também encurta o caminho de depuração, pois o estado reside em menos lugares. O custo total de propriedade tende a cair, especialmente em estágios iniciais. Essa economia libera tempo para resolver problemas de produto.

Quando ainda faz sentido usar Redis

Existem cenários em que o Redis permanece a melhor ferramenta. Esses casos exigem latência mínima difícil de replicar fora da memória pura. Outros dependem de estruturas de dados específicas e operações atômicas multi-chave. Em certos picos de fan-out, a especialização do Redis é valiosa. A seguir, apresenta-se uma lista objetiva de critérios para mantê-lo.

  • p99 sub-milisegundo como requisito rígido e não negociável.
  • Conjunto de dados quente acima de centenas de megabytes em memória.
  • Uso intensivo de estruturas como conjuntos ordenados, streams e HyperLogLog.
  • Fan-out de milhões de mensagens pub/sub por minuto.
  • Dependência de scripts Lua ou operações atômicas envolvendo múltiplas chaves.

Fora desses cenários, o PostgreSQL cobre a maioria das necessidades com conforto. Em especial, filas com SKIP LOCKED, cache com UNLOGGED e pub/sub com LISTEN/NOTIFY resolvem rotinas diárias. A simplicidade operacional passa a ser um diferencial relevante. Em equipes pequenas, isso reduz falhas por configuração e alinhamento. O benefício se traduz em foco no que importa para o negócio.

Boas práticas para operar os recursos no PostgreSQL

Alguns cuidados elevam a confiabilidade e a performance desses padrões. Conexões dedicadas são recomendadas para LISTEN/NOTIFY, evitando interferência de poolers em modo transacional. Índices específicos por expiração e status mantêm o plano de execução enxuto. Limpezas periódicas simples evitam crescimento descontrolado de tabelas efêmeras. A seguir, apresenta-se uma lista de práticas recomendadas.

  • Dimensionar e monitorar o pool de conexões, reservando conexões para ouvintes.
  • Manter índices por colunas de expiração, status e horários de execução.
  • Agendar remoções de expirados em cadência curta para preservar cache quente.
  • Adotar partição por tempo para filas e logs com alto volume.
  • Instrumentar métricas de idade de filas, taxa de falhas e consumo por trabalhador.

Alguns pontos de atenção complementam esse checklist. Tabelas UNLOGGED não são replicadas via WAL, portanto destinam-se apenas a dados efêmeros. Para NOTIFY, mensagens grandes devem virar referência a linhas para evitar truncamento e manter consistência. Em filas, transações curtas reduzem contenção e mantêm throughput estável. Em rate limiting, aproximar janelas com agregação por minuto equilibra custo e precisão. Com esses cuidados, o desenho torna-se previsível e robusto.

Exemplos completos de ponta a ponta

Esta seção reúne fluxos completos combinando leitura de cache, fallback para consulta e preenchimento de cache. O primeiro exemplo ilustra o padrão get-or-set, comum em APIs de leitura intensiva. Em seguida, um produtor e consumidor de fila demonstram a orquestração simples baseada no banco. Os exemplos usam identificadores em português para reforçar a clareza. O objetivo é oferecer blocos prontos para adaptação.

O padrão get-or-set reduz a carga sobre consultas pesadas quando há alto reuso de resultados. Essa técnica costuma ser suficiente para melhorar p95 sem camadas externas. O código em Python demonstra leitura condicional, recomputação e escrita com TTL. A escolha do TTL equilibra frescor e eficiência. O fluxo a seguir cobre o caso comum de cache de perfil.

# Get-or-set de cache com UNLOGGED e TTL
import os
import psycopg
from datetime import timedelta

def abrir_conexao():
    return psycopg.connect(os.environ["DATABASE_URL"])

def consultar_perfil_bd(conexao, usuario_id):
    with conexao.cursor() as cur:
        cur.execute("SELECT id, nome, plano FROM usuarios WHERE id = %s", (usuario_id,))
        linha = cur.fetchone()
        if not linha:
            return None
        return {"id": linha[0], "nome": linha[1], "plano": linha[2]}

def obter_perfil(usuario_id):
    with abrir_conexao() as conexao, conexao.cursor() as cur:
        # 1) Tentar cache
        chave = f"usuario:{usuario_id}:perfil"
        cur.execute("""
          SELECT valor FROM cache_store
          WHERE chave = %s AND expira_em > now()
        """, (chave,))
        achado = cur.fetchone()
        if achado:
            return achado[0]

        # 2) Fallback: consultar fonte de verdade
        perfil = consultar_perfil_bd(conexao, usuario_id)
        if perfil is None:
            return None

        # 3) Repreencher cache com TTL
        cur.execute("""
          INSERT INTO cache_store (chave, valor, expira_em)
          VALUES (%s, %s, now() + interval '15 minutes')
          ON CONFLICT (chave) DO UPDATE
            SET valor = EXCLUDED.valor,
                expira_em = EXCLUDED.expira_em
        """, (chave, perfil))
        conexao.commit()
        return perfil

O fluxo de fila combina inserção de trabalhos e consumo com retentativa. Esse exemplo evidencia a simplicidade do pipeline quando toda a coordenação é transacional. Sem serviços externos, a depuração recai em uma única fonte de logs e consultas. A seguir, inclui-se um produtor que agenda trabalhos. O consumidor já foi mostrado anteriormente.

-- Inserir trabalho na fila
INSERT INTO fila_trabalhos (tipo, carga)
VALUES ('enviar_email', jsonb_build_object('destinatario','ana@example.com','assunto','Olá'));

Métricas práticas: latência percebida e throughput

Medições realistas mostram que o Redis é mais rápido por operação de cache, como esperado de um armazenamento em memória. Ainda assim, em muitas APIs com respostas de 50–200 ms, diferenças de sub-milisegundo no cache perdem relevância. Quando cache e dados relacionais estão no mesmo banco, elimina-se um salto de rede. Essa eliminação pode aproximar a latência efetiva entre as soluções. Em picos, a previsibilidade transacional do PostgreSQL simplifica sustentação.

Ao avaliar, convém medir p50, p95 e p99 sob carga de concorrência típica. A taxa de transferência (req/s) deve ser observada diante de padrões de leitura e escrita do produto. Testes com aquecimento de cache e dados realistas produzem resultados mais confiáveis. A decisão não se resume ao número menor, e sim à suficiência para o SLA. Em muitos casos, a simplicidade vence pequenas vantagens de latência bruta.

Conclusão

Os recursos modernos do PostgreSQL tornam o Redis opcional em grande parte das necessidades de startups. Tabelas UNLOGGED entregam cache rápido, LISTEN/NOTIFY habilita pub/sub leve e SKIP LOCKED resolve filas com segurança transacional. O suporte a JSONB e JSON_TABLE amplia flexibilidade sem abandonar o SQL. A centralização reduz custos, simplifica operação e encurta o caminho de depuração. Em cenários extremos de latência e estruturas especializadas, o Redis continua imbatível, mas fora deles um único banco bem configurado costuma ser o suficiente.