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.