Acesso rápido a dados no Node.js com Valkey
Aprenda a usar Valkey para otimizar o cache e acesso a dados em aplicações Node.js.

Este tutorial implementa uma camada avançada de cache em uma aplicação Node.js usando Valkey, um datastore de alto desempenho compatível com Redis. Exploramos cache em camadas, invalidação de cache e nomes de chave em uma arquitetura modular com Express. A configuração assume uma aplicação multi-serviço onde respostas de API precisam ser armazenadas em cache com flexibilidade e controle de expiração.
Casos reais: Cache avançado com Valkey
O Valkey nasceu como um fork open-source do Redis, apoiado por AWS, Google e Oracle, após a mudança de licença do Redis. No desenvolvimento de web apps, o desempenho do frontend depende da rapidez e consistência do backend. O cache avançado com Valkey é crucial quando o frontend depende de dados dinâmicos, como preferências do usuário ou conteúdo personalizado.
Este tutorial é útil se você está:
- Construindo uma aplicação que manipula diferentes tipos de dados com requisitos variados de atualização, como uma plataforma de ecommerce com catálogos de produtos e perfis de usuários.
- Desenvolvendo uma plataforma de conteúdo que serve posts de blog ou artigos para uma grande audiência.
- Trabalhando com dashboards que exibem métricas em tempo real e dados de fundo.
Vamos começar!
Instalando o Valkey e o cliente Node.js
Valkey é compatível com o protocolo Redis, então qualquer cliente Redis funciona. Usaremos ioredis
por seu suporte a comandos avançados e gerenciamento de clusters:
docker run -d --name valkey -p 6379:6379 valkey/valkey npm install ioredis express
Crie um novo arquivo lib/cache.js
para a instância do cliente Valkey:
// lib/cache.js const Redis = require('ioredis'); const redis = new Redis({ host: '127.0.0.1', port: 6379, // estratégia de retry ou config de autenticação podem ser adicionadas aqui }); module.exports = redis;
Esta abstração permite reutilizar a instância redis
em vários serviços e configurar facilmente mocks de teste.
Implementando cache em camadas com namespaces e TTL
Esta seção constrói uma utilidade de cache de propósito geral que fornece geração estruturada de chaves, suporte a TTL e invalidação seletiva por meio de namespaces. É projetado para cenários onde múltiplos serviços ou domínios compartilham a mesma instância do Valkey, mas requerem isolamento e controle sobre suas respectivas entradas de cache.
O módulo suporta quatro operações: get
, set
, del
e clearNamespace
. Todas as chaves de cache seguem o padrão :
, permitindo buscas rápidas e invalidação escopada. Os TTLs garantem que dados obsoletos sejam removidos automaticamente, mesmo sem exclusão explícita.
Crie lib/cacheUtil.js
e adicione a seguinte implementação:
// lib/cacheUtil.js const redis = require('./cache'); /** * Gera uma chave de cache totalmente qualificada usando um namespace e um identificador único. * Isso evita colisões entre tipos de dados não relacionados armazenados na mesma instância do Valkey. */ function getCacheKey(namespace, id) { return `${namespace}:${id}`; } /** * Recupera um valor em cache por namespace e id. * Retorna null se a chave estiver ausente ou se a análise JSON falhar. */ async function get(namespace, id) { const key = getCacheKey(namespace, id); const data = await redis.get(key); if (!data) return null; try { return JSON.parse(data); } catch (err) { // Opcionalmente, registre ou trate erros de análise JSON se os dados estiverem corrompidos return null; } } /** * Armazena em cache um valor em um determinado namespace e id com um TTL configurável. * O TTL é especificado em segundos. O padrão é 60 segundos. */ async function set(namespace, id, value, ttl = 60) { const key = getCacheKey(namespace, id); const json = JSON.stringify(value); await redis.set(key, json, 'EX', ttl); } /** * Exclui uma entrada de cache específica por namespace e id. * Usado durante mutações de dados para remover entradas obsoletas. */ async function del(namespace, id) { const key = getCacheKey(namespace, id); await redis.del(key); } /** * Exclui todas as chaves de cache que pertencem a um determinado namespace. * Isso é útil para invalidação em massa, por exemplo, limpando todos os caches de produtos após uma atualização de preço. * Internamente, usa o comando KEYS, que deve ser usado com cautela em grandes conjuntos de dados. */ async function clearNamespace(namespace) { const pattern = `${namespace}:*`; const keys = await redis.keys(pattern); if (keys.length > 0) { await redis.del(...keys); } } module.exports = { get, set, del, clearNamespace };
Essa utilidade fornece uma interface limpa e extensível para a lógica de cache em toda a aplicação. Por exemplo, se sua aplicação armazena em cache perfis de usuários com IDs como user:123
e você atualiza as informações do usuário, pode invalidar essa entrada de cache específica sem afetar outras:
await cache.del('user', '123'); // remove apenas o cache para o usuário 123
A função clearNamespace
é especialmente útil para cenários como migrações de dados ou atualizações em lote. Por exemplo, após atualizar todos os preços dos produtos, chamar clearNamespace('product')
garante que nenhum dado de produto desatualizado permaneça no cache.
O suporte a TTL garante expiração automática, o que é crucial em sistemas distribuídos onde a invalidação do cache pode falhar ocasionalmente. Você pode variar o TTL por caso de uso, por exemplo, TTLs curtos para dados voláteis como preços e TTLs mais longos para dados estáveis como códigos de país ou flags de recursos.
Essa utilidade abstrai o boilerplate repetitivo de manipulação de JSON, formatação de chaves e lógica de TTL, mantendo o restante do seu código focado na lógica de negócios em vez da mecânica de cache.
Cache baseado em middleware para manipuladores de rotas
Esta seção apresenta uma função middleware para Express que adiciona cache em nível de rota com Valkey. Ele permite o cache automático de respostas para qualquer endpoint GET
, armazenando e recuperando payloads JSON serializados sob chaves baseadas no URL original da solicitação.
Você usaria tipicamente cache baseado em middleware como este ao construir uma plataforma de conteúdo que serve posts de blog ou artigos para uma grande audiência. Por exemplo, quando os usuários visitam posts populares ou navegam em páginas de categoria, essas solicitações geralmente atingem os mesmos dados repetidamente. Em vez de consultar o banco de dados toda vez que alguém carrega um post, o middleware armazena o JSON completo da resposta para aquela rota, tornando visitas repetidas extremamente rápidas.
Essa abordagem elimina a lógica de cache repetitiva em cada manipulador de rota, mantendo controle granular por meio de segmentação de namespace e configuração de TTL.
Crie um arquivo middleware/cacheMiddleware.js
:
// middleware/cacheMiddleware.js const cache = require('../lib/cacheUtil'); function cacheMiddleware(namespace, ttl = 60) { return async (req, res, next) => { const cacheKey = req.originalUrl; const cached = await cache.get(namespace, cacheKey); if (cached) { return res.json({ data: cached, cached: true }); } res.sendJson = res.json; res.json = async (body) => { await cache.set(namespace, cacheKey, body, ttl); res.sendJson({ data: body, cached: false }); }; next(); }; } module.exports = cacheMiddleware;
Esta abordagem intercepta a resposta e a armazena no Valkey se ainda não estiver em cache. O namespace
garante isolamento entre diferentes grupos de rotas.
Exemplo de uso em uma rota Express
Assumindo que estamos buscando dados de produtos de um banco de dados. Crie a seguinte rota em routes/products.js
:
// routes/products.js const express = require('express'); const router = express.Router(); const cacheMiddleware = require('../middleware/cacheMiddleware'); // Chamada de DB mock async function getProductFromDB(id) { await new Promise((r) => setTimeout(r, 100)); // simula latência return { id, name: `Product ${id}`, price: Math.random() * 100 }; } router.get('/:id', cacheMiddleware('product', 120), async (req, res) => { const product = await getProductFromDB(req.params.id); res.json(product); }); module.exports = router;
Integre isso ao servidor principal:
// server.js const express = require('express'); const app = express(); const productRoutes = require('./routes/products'); app.use('/api/products', productRoutes); app.listen(3000, () => console.log('Server running on port 3000'));
Cada solicitação GET
para /api/products/:id
primeiro verifica o Valkey. Se não estiver presente, busca do DB e armazena o resultado em cache.
Invalidação de cache durante mutações
Invalide entradas de cache obsoletas sempre que a fonte de dados mudar. Adicione uma rota de atualização no mesmo arquivo:
// routes/products.js (continuação) const cache = require('../lib/cacheUtil'); router.put('/:id', async (req, res) => { const updated = { id: req.params.id, ...req.body }; // Assume atualização de banco de dados aqui await cache.del('product', `/api/products/${req.params.id}`); res.json({ updated, invalidated: true }); });
Isso limpa apenas a entrada de cache do produto afetado. Use clearNamespace
se todas as entradas de um modelo precisarem ser redefinidas, como após importações em massa.
Para rodar os exemplos, você deve ter a seguinte estrutura:
valkey-caching-demo/ ├── lib/ │ ├── cache.js │ └── cacheUtil.js ├── middleware/ │ └── cacheMiddleware.js ├── routes/ │ └── products.js └── server.js
Inicie o servidor usando:
node server.js
Você deve ver:
Server running on port 3000
Use curl
para testar o comportamento do cache. Busque o produto com ID 1
(isso simulará uma chamada de DB e então armazenará a resposta em cache):
curl http://localhost:3000/api/products/1
A resposta será semelhante a:
{ "data": { "id": "1", "name": "Product 1", "price": 47.38 }, "cached": false }
Repita a mesma solicitação (desta vez ela será atendida do cache Valkey):
curl http://localhost:3000/api/products/1 { "data": { "id": "1", "name": "Product 1", "price": 47.38 }, "cached": true }
Atualize o produto e acione a invalidação de cache:
curl -X PUT http://localhost:3000/api/products/1 -H "Content-Type: application/json" -d '{"name": "Updated Product 1"}'
A próxima solicitação GET
buscará e armazenará novamente os dados atualizados em cache.
O aplicativo testa o cache em nível de rota com TTL, recuperação e invalidação de cache usando Valkey em uma configuração Node.js Express. Verifica se as solicitações GET
são armazenadas em cache, as solicitações PUT
limpam dados obsoletos e a segmentação de chaves isola as entradas de cache.
Usando pub/sub do Valkey para coordenação de cache entre instâncias
Em implantações distribuídas com várias instâncias Node.js (por exemplo, atrás de um balanceador de carga), uma invalidação de cache local afeta apenas a instância onde a mutação ocorre. Outras instâncias mantêm entradas de cache obsoletas a menos que um mecanismo de invalidação coordenada esteja em vigor.
O sistema nativo de publicação/assinatura do Valkey fornece uma solução leve para transmissão de eventos de invalidação de cache em todas as instâncias em execução. Cada instância assina um canal compartilhado e escuta mensagens de invalidação. Quando uma mensagem é recebida, a instância exclui a entrada de cache correspondente de sua conexão local do Valkey.
Estenda o cliente Valkey com lógica de pub/sub
Atualize o cliente Valkey em lib/cache.js
para incluir três conexões:
- Uma para operações gerais de cache
- Um assinante (
sub
) escutando eventos de invalidação - Um publicador (
pub
) para emitir eventos de invalidação:// lib/cache.js
const Redis = require('ioredis'); const redis = new Redis(); // Conexão principal const sub = new Redis(); // Conexão de assinante const pub = new Redis(); // Conexão de publicador // Assine eventos de invalidação sub.subscribe('cache-invalidate'); // Ouça mensagens e delete chaves de cache correspondentes sub.on('message', async (channel, message) => { if (channel === 'cache-invalidate') { try { const payload = JSON.parse(message); const { namespace, key } = payload; const fullKey = `${namespace}:${key}`; await redis.del(fullKey); console.log(`Cache invalidated: ${fullKey}`); } catch (err) { console.error('Invalidation message parse error:', err); } } }); // Usado em manipuladores de API para acionar invalidação function publishInvalidation(namespace, key) { const message = JSON.stringify({ namespace, key }); pub.publish('cache-invalidate', message); } module.exports = { redis, publishInvalidation };
Por que mensagens JSON?
Usar uma estrutura JSON { namespace, key }
em vez de strings brutas como 'product:/api/products/123'
evita ambiguidades de análise e facilita a extensão do formato da mensagem mais tarde (por exemplo, incluir invalidateAll: true
).
Modifique a rota PUT
para transmitir a invalidação
Atualize o manipulador de atualização de produto em routes/products.js
para notificar todas as instâncias da aplicação quando um produto for atualizado:
// routes/products.js (dentro do router.put) const { publishInvalidation } = require('../lib/cache'); router.put('/:id', async (req, res) => { const updated = { id: req.params.id, ...req.body }; // Simule atualização de DB aqui const cacheKey = `/api/products/${req.params.id}`; // Invalidação local (redundante, mas rápida) await cache.del('product', cacheKey); // Invalidação entre instâncias publishInvalidation('product', cacheKey); res.json({ updated, invalidated: true }); });
Cada instância receberá a mensagem cache-invalidate
e excluirá sua entrada de cache correspondente, garantindo que todos os ambientes permaneçam sincronizados.
Lance múltiplas instâncias de aplicativo (para teste local)
Você pode simular um ambiente distribuído usando duas sessões de terminal:
# Terminal 1 PORT=3000 node server.js # Terminal 2 PORT=3001 node server.js
Ambas as instâncias se conectarão ao mesmo servidor Valkey. Quando uma solicitação PUT
é enviada para uma instância, ambas responderão à invalidação pub/sub:
curl -X PUT http://localhost:3000/api/products/1 -H "Content-Type: application/json" -d '{"name": "Updated Product"}'
Se você adicionar um console.log()
no manipulador sub.on('message')
, ambos os terminais registrarão a exclusão da chave de cache.
Conclusão
Esta implementação cria uma camada de cache modular baseada em Valkey para uma aplicação Node.js. Suporta cache em nível de middleware com TTL, gerenciamento de chaves baseado em namespace e invalidação automática durante mutações. O suporte a pub/sub garante consistência em implantações escaladas horizontalmente. Esta estrutura oferece controle granular sobre o comportamento do cache, permanecendo adaptável a topologias de serviço complexas.