Categorias do Site

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.

Logotipo azul em formato de casa com uma chave no centro sobre um fundo amarelo texturizado.

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

advanced caching in Node.js with 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.

  • Ferramentas de IA falham em contexto: soluções

    Ferramentas de IA em programação falham em contexto. Descubra como contornar esse problema e otimizar seu uso.

    Ferramentas de IA em programação falham em contexto. Descubra como contornar esse problema e otimizar seu uso.

    Ler notícia completa
    Ícone de rosto humano recortado em perfil com engrenagem e circuito integrado sobre fundo texturizado cinza, simbolizando tecnologia e inteligência.
  • Ações Photoshop para Efeitos de Esboço em 2025

    Descubra ações do Photoshop que transformam fotos em esboços artísticos de forma fácil e rápida.

    Descubra ações do Photoshop que transformam fotos em esboços artísticos de forma fácil e rápida.

    Ler notícia completa
    Desenho em azul de duas mulheres estilizadas, uma olhando de lado com expressão séria e a outra posando com um chapéu largo e um vestido justo. Logotipo do Photoshop no canto.
  • Erro ao Ignorar Pesquisa UX em Robo Advisor

    Descubra como a falta de pesquisa UX sabotou nosso projeto de robo advisor e a importância de ouvir os usuários.

    Descubra como a falta de pesquisa UX sabotou nosso projeto de robo advisor e a importância de ouvir os usuários.

    Ler notícia completa
    Ilustração estilizada de livros empilhados em tons de roxo, sobre fundo texturizado que imita papel envelhecido.
  • A transição silenciosa para Vite e seu impacto

    Este ano, Vite ultrapassou 140 milhões de downloads semanais, superando o Webpack e continuando sua tendência de crescimento. É surpreendente, considerando que Webpack foi o principal bundler para JavaScript por muito tempo. Com o Vite, os desenvolvedores agora desfrutam de tempos de construção mais rápidos e recargas automáticas instantâneas. Para entender essa mudança, vamos observar […]

    Vite é adotado em larga escala, ultrapassando Webpack com sua rapidez e simplicidade. Descubra o impacto dessa transição.

    Ler notícia completa
    Logo colorido em forma de raio sobre um fundo de folha verde com listras brancas.
  • O colapso do Stack Overflow e o impacto da IA

    A programação sempre foi um desafio complexo, e os desenvolvedores frequentemente recorrem a comunidades online, como o Stack Overflow, para encontrar soluções para problemas de desenvolvimento. No entanto, com o lançamento do ChatGPT no final de 2022, o uso do Stack Overflow começou a diminuir. Ferramentas de IA generativa passaram a oferecer respostas instantâneas para […]

    Stack Overflow em declínio com a ascensão da IA. Como a mudança afeta a programação e o futuro das comunidades de desenvolvedores.

    Ler notícia completa
    Logotipo estilizado de cor laranja sobre fundo texturizado preto que lembra uma superfície rochosa ou um muro descascado.
  • AI Runtimes: O Futuro Além das Planilhas

    Por décadas, softwares empresariais foram construídos em torno de operadores humanos. Ferramentas como planilhas, sistemas de CRM e softwares de call center se tornaram a estrutura invisível das organizações modernas. No entanto, com o avanço da IA, a questão é: por que ainda precisamos de planilhas? O futuro do trabalho será definido por runtimes de […]

    AI agent runtimes substituirão ferramentas tradicionais, revolucionando o ambiente de trabalho.

    Ler notícia completa
    Placas de trilha com ícones representando tecnologia e colaboração humana penduradas em uma trilha florestal.
  • Crie app de IA multimodal com Next.js

    Os modelos de linguagem grandes evoluíram rapidamente de texto simples para multimodais, agora processando imagens, áudio e até vídeos em tempo real. Este tutorial ensina como integrar essas capacidades em apps com Next.js e a API Gemini. Neste artigo, você aprenderá a criar interações de IA multimodal com Next.js e Gemini. Vamos abordar como lidar […]

    Aprenda a construir um app de IA multimodal com voz e visão em Next.js usando a API Gemini.

    Ler notícia completa
    Dois fones de ouvido com microfone pendurados, com um ícone de proibido superposto, contendo a letra N.
  • Proteja IA de ataques de injeção de prompts

    Com os modelos de linguagem de grande porte (LLMs) se tornando essenciais em aplicações de IA, como bots de serviço ao cliente e assistentes de codificação, surgem vulnerabilidades de segurança: ataques de injeção de prompts. Esses ataques exploram a capacidade dos LLMs de interpretar linguagem natural para manipular o comportamento dos agentes de maneira indesejada […]

    Aprenda a proteger agentes de IA contra ataques de injeção de prompts usando seis padrões de design eficazes.

    Ler notícia completa
    Ilustração de rede digital com nós conectados por linhas e ícones simbolizando alertas e mensagens sobre um fundo quadriculado azul.
  • AI e Acessibilidade: Prioridades no Design UX

    Como designers web, aprendemos a priorizar o design centrado no humano. No entanto, os novos usuários da web não são apenas pessoas, mas agentes inteligentes. Seja um assistente de voz navegando em seu site, um modelo de AI resumindo suas páginas de produtos ou um agente autônomo reservando uma viagem, as experiências digitais não são […]

    AI e acessibilidade são essenciais no design UX moderno. Veja como criar experiências inclusivas para humanos e máquinas.

    Ler notícia completa
    Ilustração de um robô futurista com a sigla