Executando verificação de segurança...
22

Conheça esta técnica avançada de NodeJS

Em cenários aonde o throughput é muito alto, a solução mais comumente utilizada é adicionar uma camada de caching, seja no client side para os assets, seja no backend para os dados.
No entanto, existe uma combinação de duas técnicas que pode ser usada de forma isolada em recursos com alto throughput, que aplica um nível mais básico de caching e que a depender do cenário, pode reduzir o tempo de resposta das requisições.
Esta combinação de técnicas se chama: Asynchronous Request Batching and Caching, que consiste em resolver as requisições em lotes e fazer cache em memória dos resultados.
Para explicar como ela funciona, vou ilustrar como geralmente são os fluxos de requisição, e em seguida, vou apresentar uma variação da ilustração aplicando a técnica de batching acompanhada do código, e em seguida, farei o mesmo aplicando a combinação do cache em memória.

Duas requisições assíncronas (sem as técnicas)

sem as técnias

Observe que cada requisição invoca a função de getProductsByCategory e cria um fluxo individual de requisição e resposta. Então, a consulta no banco de dados será replicada para cada uma das requisições.
Agora, veja o que acontece quando aplicamos a técnica de requisições em lote (batching).

Aplicando a técnica de Requisições em Lote (batching)

com batching

Implementação da técnica

import getProductsByCategoryRaw from "./getProductsByCategoryRaw.js"

const requestsBatch = new Map()

async function getProductsByCategory(category) {
    if (requestsBatch.has(category)) {
        return requestsBatch.get(category)
    }

    const result = getProductsByCategoryRaw(category)
    requestsBatch.set(category, result)
    result.finally(()=>{
      requestsBatch.delete(category)
    })

    return result
}

export default getProductsByCategory

Veja que a função getProductsByCategoryRaw (que de fato faz a consulta no banco) retorna uma Promise. Salvamos a Promise em um Map utilizando como chave a própria categoria do produto, e isso garante que as requisições seguintes terão acesso à mesma Promise que ainda não foi resolvida. Quando ela for resolvida, retornará o mesmo resultado para todas as requisições do lote, e o finally vai remover a Promise do mapa.

Combinando batching com caching

com batching e caching

Implementação da técnica

import getProductsByCategoryRaw from "./getProductsByCategoryRaw.js"

const CACHE_TTL = 30 * 1000
const cache = new Map()

async function getProductsByCategory(category) {
    if (cache.has(category)) {
        return cache.get(category)
    }

    const result = getProductsByCategoryRaw(category)
    cache.set(category, result)
    result.then(() => {
        setTimeout(() => {
            cache.delete(category) 
        }, CACHE_TTL)
    }, err => {
        cache.delete(category)
        throw err
    })

    return result
}

export default getProductsByCategory

Comparando com o exemplo anterior, a lógica é bem parecida, porém, ao invés de imediatamente remover a Promise resolvida do mapa, mantemos ela em memória por um tempo de vida (que no caso do exemplo foi de 30 segundos). O que significa que as requisições que acontecerem no período de agrupamento do lote, terão acesso ao resultado final quase que ao mesmo tempo (assim como no exemplo anterior, aplicando apenas o batching), e durante os 30 segundos seguintes à resolução da Promise, toda requisição terá retorno quase que imediato, pois a Promise já está resolvida no cache.
Isso é possível, porque Promises no javascript permitem acesso à seu resultado mesmo após serem resolvidas. E as técnicas acima tiram proveito disso.

Ressalvas

As técnicas que apresentei não são "bala de prata" e a estratégia de caching que apresentei é muito simples, e exige memória. Sobretudo, ela faz sentido em cenários aonde você possui uma alta taxa de transferência (throughput) em um curto espaço de tempo. Se você tiver um volume alto de requisições, mas que possuem uma distância temporal, certamente as técnicas não ajudarão em nada, e pode ser que uma camada de cache tradicional seja mais pertinente. Sempre procure entender o cenário do seu problema com métricas e análises delas.
Bons estudos e um forte abraço!

1

Salvando o conteúdo para consumir posteriormente com mais calma, já que não é todo dia que vemos conteúdos realmente avançados com Node.Js!

1

eu estava pensando nisso. Podia ter uma forma de salvar um post pra ver novamente mais tarde, tipo, adicionar nos favoritos, ou o assistir mais tarde que tem no Youtube.

1
1

No backend vc pode usar estratégia parecida de micro caching. Em sistemas com alto fluxo transacional, você pode cachear uma rota por 500ms ou talvez até por 1s, como uma home, dashboard da aplicação, principalmente rotas públicas e/ou caras de processamento.

Assim se você tiver 1k, 100k ou 1M de usuários online, o primeiro pega a rota, os demais pegarão somente o resultado do processamento depois que o lock do primeiro processo decair, e a aplicação terá sempre o aspecto de real time, pq 500ms ou 1s de cache é praticamente imperceptível.

1

Ótimo conteúdo! Sempre usei o cache tradicional salvando o resultado da promise em cache, vou fazer alguns testes com a técnica que ensinou no post.
Percebeu ganhos de consumo de recursos ou velocidade usando essa técnica em comparação com o cache tradicional?

1

Boa pergunta. A relação de custo-benefício aqui está muito atrelada ao cenário. Em termos de velocidade, diria que a comparação entre uma técnica e outra não te diz muito caso o cenário serja favorável ao Batching/Caching das Promises que apresentei no texto.
Por outro lado, se houver um espaço considerável entre os volumes de requisições, acredito que a aplicação da técnica não faça sentido.
Por exemplo, há um tempo atrás, apliquei esta técnica em um recurso de API que era consumido por vários clientes (isso inclui apps e outras APIs), e que tinha uma explosão de consumo (throughput) de segunda a sexta, sempre nas 2 primeiras horas do dia. Ao usarmos essa técnica, reduzimos muito a taxa de leitura do banco de dados e melhoramos muito o tempo de resposta.
No entanto, esse recurso começou a ser consumido por outros clientes, e isso alterou a dinâmica de consumo do recurso, e agora além das 2 horas iniciais, havia um aumento no volume, porém as variações de volume eram intermitentes ao ponto que as técnicas já não ajudavam tanto.
Foi então que implantar uma camada de cache se fez necessário.
O lado bom é que poupamos dinheiro com infra e com pessoas para cuidarem dela, durante um tempo até que o uso de um cache tradicional se fez necessário e aí não teve pra onde correr mais.