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.

  • Enfrentando a Complexidade com GraphQL

    Descubra como GraphQL facilita o desenvolvimento de soluções inteligentes com IA.

    Descubra como GraphQL facilita o desenvolvimento de soluções inteligentes com IA.

    Ler notícia completa
    Banner de podcast da UX Magazine intitulado
  • UX: Emoções Além das Telas no Design

    Descubra como o design emocional transforma experiências, indo além das telas e criando conexões humanas.

    Descubra como o design emocional transforma experiências, indo além das telas e criando conexões humanas.

    Ler notícia completa
    Símbolo abstrato em tons de marrom e laranja que se assemelha a uma pessoa estilizada com braços estendidos e uma perna erguida.
  • Como usar CSS line-clamp para limitar texto

    Aprenda a usar a propriedade CSS line-clamp para limitar linhas de texto e melhorar a aparência do layout.

    Aprenda a usar a propriedade CSS line-clamp para limitar linhas de texto e melhorar a aparência do layout.

    Ler notícia completa
    Fundo gradiente em tons de laranja e violeta com o texto
  • Promise.all ainda é relevante em 2025?

    Antes das promises serem introduzidas nativamente no JavaScript, usávamos muitos callbacks para tarefas assíncronas. É comum ver callbacks sendo usados, pois muitos desenvolvedores podem ainda pensar que callbacks e promises são o mesmo, mas não são. Quando promises foram introduzidas, substituíram amplamente os callbacks, tornando a sintaxe mais compreensível. Em 2025, com async/await, Promise.allSettled, Promise.any […]

    Promise.all é crucial para tarefas assíncronas, mas novas alternativas surgem em 2025. Saiba quando usá-lo.

    Ler notícia completa
    Logotipo do JavaScript (JS) em quadrado amarelo sobre fundo com ondas suaves em tons de branco e cinza claro.
  • Equilibrando IA e UX: O Desafio do Design Humanizado

    A IA está sendo integrada aos fluxos de trabalho de design modernos, ajudando na geração de conteúdo, ideação e prototipagem. Isso aumenta a eficiência das equipes de design, aprimorando a forma como criamos, pensamos e resolvemos problemas. No entanto, a IA também traz preocupações ao processo de design, como a possível perda de foco no […]

    Descubra como manter o design UX humanizado enquanto utiliza IA para otimizar processos e aumentar a produtividade.

    Ler notícia completa
    Mão robótica branca tocando a ponta do dedo de uma mão humana contra um fundo colorido em tons de arco-íris.
  • A Revolução dos Navegadores com IA: Impactos e Futuro

    Há uma revolução silenciosa ocorrendo em um software que você usa diariamente, mas raramente pensa sobre: o navegador. Chrome, Safari, Firefox têm sido nossas janelas para a web por décadas. Agora, algo significativo está acontecendo. Uma nova espécie de navegador está surgindo: o navegador com IA. Ele não apenas muda como navegamos, mas redefine o […]

    Navegadores com IA estão mudando a web, impactando a criatividade, economia e verdade online.

    Ler notícia completa
    Tela de interface do Instacart mostrando produtos essenciais para praia à venda, como protetor solar e toalhas, com uma janela de chat com o assistente virtual aberta.
  • As 3 previsões para o futuro do design UX

    A evolução tecnológica moderniza e melhora todas as áreas da tecnologia, incluindo o design de dispositivos digitais, automação, desenvolvimento de software e design UI/UX. Essa evolução e as inovações em HCI (Interação Humano-Computador) impulsionam o design UI/UX para ajudar designers a criar produtos digitais mais amigáveis, usáveis e produtivos para todos os usuários. O design […]

    Confira as três principais previsões para a próxima era do design UX e como elas podem impactar o futuro das interfaces digitais.

    Ler notícia completa
    Ilustração em 3D de um computador desktop moderno com ícones em estilo futurista na tela, sobre fundo roxo com linhas de rede digitais.
  • A Importância do Enquadramento no Design

    No design, o enquadramento do problema está se tornando o cerne do papel humano. À medida que a IA, ou o que chamo de Programa, assume mais o trabalho de solução, nosso ofício muda para como tratamos o problema. “A IA não está substituindo designers; está substituindo designers que focam em saídas automatizáveis.” Citação e […]

    Explorando como o enquadramento de problemas redefine o papel humano no design em tempos de IA.

    Ler notícia completa
    Imagem de rabisco em preto e branco cheia de palavras e desenhos, incluindo cabeças estilizadas, uma palavra
  • Psicologia Ética no E-commerce: Facilite Compras

    A psicologia no e-commerce tem uma má reputação, muitas vezes associada a táticas de manipulação como escassez artificial e cobranças ocultas. No entanto, existe um lado positivo: a facilitação das compras sem manipulação. Trabalhando anos com e-commerce, percebi que a maioria dos problemas de conversão está em facilitar o processo de compra. Vou mostrar quatro […]

    Aprenda como remover barreiras psicológicas no e-commerce, promovendo compras éticas sem manipulação.

    Ler notícia completa
    Ilustração de um trator removendo neve da estrada, com carros vermelhos parcialmente cobertos de neve ao lado. Ambiente frio com árvores ao fundo.