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.

  • 10 Melhores Temas de Landing Page para WordPress 2025

    Descubra os 10 melhores temas grátis para landing pages no WordPress em 2025 e otimize suas conversões.

    Descubra os 10 melhores temas grátis para landing pages no WordPress em 2025 e otimize suas conversões.

    Ler notícia completa
    Banner colorido promovendo
  • Agentes de IA no Atendimento: Suporte 24/7 Sem Estresse

    Descubra como agentes de IA melhoram o atendimento ao cliente, oferecendo suporte contínuo e reduzindo o estresse dos funcionários.

    Descubra como agentes de IA melhoram o atendimento ao cliente, oferecendo suporte contínuo e reduzindo o estresse dos funcionários.

    Ler notícia completa
    Imagem abstrata de linhas curvas em tons de preto formando um túnel, com texto sobre inteligência artificial em serviços ao cliente.
  • Por que o Gemini falha em atender os usuários

    Gemini promete melhorias, mas falha em desempenho real, levantando questões sobre IA e design centrado no usuário.

    Gemini promete melhorias, mas falha em desempenho real, levantando questões sobre IA e design centrado no usuário.

    Ler notícia completa
    Robô com aparência humana apresenta um roteiro de inteligência artificial em um grande monitor para um público em uma sala de reunião.
  • Plataformas de execução para agentes em ascensão

    Com o avanço da IA de prompts únicos para comportamentos autônomos e persistentes, uma nova classe de infraestrutura está surgindo: runtimes para agentes. Diferente de aplicativos ou plataformas tradicionais, são ambientes de execução projetados para criar, executar e orquestrar agentes de IA capazes de autonomia, uso de ferramentas e colaboração. Existem dois tipos principais de […]

    Descubra as novas plataformas de execução para agentes de IA, suas funcionalidades e como estão transformando o desenvolvimento autônomo.

    Ler notícia completa
    Ilustração estilizada de um homem de camisa laranja e um robô com detalhes em vermelho, separados por um chip entre eles, fundo azul.
  • Melhores Presets de Animação de Texto para Premiere

    A animação de texto vai além da decoração. Ela controla o ritmo, mantém a atenção e adiciona ritmo à edição. Uma palavra bem sincronizada na tela pode fazer mais do que uma transição sofisticada. Seja você criador de conteúdo para YouTube, tutorias detalhados, promos de produtos, reels, vlogs ou sequências de títulos, uma boa tipografia […]

    Descubra os melhores presets de animação de texto para aprimorar seus vídeos no Premiere Pro e encantar seu público.

    Ler notícia completa
    Imagem de uma paisagem com um viaduto, em tons de cinza, com o texto
  • Liderança: Relações Pessoais com Lars Rieger

    Lars Rieger é chefe de produto na Digistore24 DACH, uma plataforma de revenda digital e marketing de afiliados. Ele começou sua carreira como gerente de projetos na Digistore24 e foi promovido a gerente de produto líder. Após uma passagem pela CarOnSale, ele voltou à Digistore24 em 2024. Em nossa conversa, Lars fala sobre a importância […]

    Lars Rieger fala sobre a importância das relações em gestão de produtos e transformação digital na Digistore24.

    Ler notícia completa
    Imagem promocional de Lars Rieger, identificado como Head of Product na Digistore24, com logo da LogRocket. Design gráfico em tons de azul com linhas geométricas.
  • Melhores Templates de Cartões de Visita para 2025

    No mundo digital, cartões de visita ainda são essenciais para networking profissional. Eles criam uma impressão duradoura e influenciam a imagem profissional. O design correto de cartões de visita é crucial para ser lembrado positivamente. O Adobe InDesign é a escolha preferida para criar cartões de visita, oferecendo controle preciso de layout e design. Com […]

    Explore mais de 30 templates de cartões de visita para InDesign em 2025, perfeitos para diversas indústrias e estilos.

    Ler notícia completa
    Pilha de cartões de visita em tons de roxo com logotipo branco
  • Destaque: Medindo o Imensurável com Paul Weston

    Paul Weston é GM & VP de Produto na HubSpot, liderando a visão e estratégia do Service Hub. Em sua conversa, ele fala sobre “medir o imensurável”, trazendo dados objetivos para elementos difíceis de quantificar. Ele destaca a importância de focar nas necessidades de um conjunto específico de clientes e como pensa em diferenciação de […]

    Paul Weston discute como medir o imensurável em UX, focando em dados objetivos e necessidades dos clientes.

    Ler notícia completa
    Texto alternativo: Imagem promocional de Paul Weston, GM e VP de Produto na HubSpot, com pano de fundo gráfico em tons de azul e logos da LogRocket e HubSpot.
  • Conversando sobre o uso de Spinners no UX

    Olha, Spinner, já passamos por muita coisa juntos… Eu sei que você está fazendo o seu melhor — girando sem parar, pacientemente preenchendo o vazio, corajosamente mascarando o caos do backend como um técnico de palco sobrecarregado em uma produção teatral falida. Mas ambos sabemos que isso não funciona mais. No início, você era reconfortante. […]

    Spinners enfrentam críticas no UX. Progressos e alternativas como barras de progresso ganham espaço.

    Ler notícia completa
    Gráfico de evolução de círculos de carregamento, variando de incompletos a completos em tons de cinza, sobre fundo preto.