При разработке серверных приложений в Node.js часто необходимо кэшировать данные, получаемые с удаленных серверов. Например, нам может понадобиться кэшировать данные конфигурации, полученные от удаленного API, которые редко меняются. В этой статье мы рассмотрим, как правильно кэшировать удаленно полученные данные в Node.js и избежать распространенной ошибки.

Проблема

Например, у нас есть функция, которая извлекает данные конфигурации из удаленного API и возвращает их как обещание, давайте просто назовем ее `fetchConfig()` и дадим ей следующую фиктивную реализацию:

function fetchConfig() {
    return Promise.resolve({
        pageSize: 10,
    });
}

Теперь мы хотим кэшировать данные конфигурации в памяти, чтобы нам не приходилось извлекать их снова и снова. Мы можем сделать это, сохранив данные конфигурации в переменной и вернув их из функции, если они уже установлены:

let config;
async function fetchCachedConfig() {
   if (config) {
       return config;
   }
   config = await fetchConfig();
   return config;
}

Давайте проверим, работает ли кеш должным образом:

Случай №1: последовательные запросы

it(“Should be called only once when several requests fired sequentially”, async () => {
   await fetchCachedConfig();
   const conf = await fetchCachedConfig();
   expect(fetchConfigSpiedOn).toHaveBeenCalledTimes(1);
});

Случай 2: одновременные запросы

it(“Should be called only once when several requests fired concurrently”, async () => {
   const conf = await Promise.all([fetchCachedConfig(), fetchCachedConfig()]);
   expect(fetchConfigSpiedOn).toHaveBeenCalledTimes(1);
});

Результат теста

✓ Should be called only once when several requests fired sequentially (2 ms)
✕ Should be called only once when several requests fired concurrently (1 ms)

● Should be called only once when several requests fired concurrently
expect(jest.fn()).toHaveBeenCalledTimes(expected)
Expected number of calls: 1
Received number of calls: 2

Первый тест проходит, а второй не проходит. Причина в том, что когда два запроса запускаются одновременно, переменная `config` еще не установлена ​​при обработке второго запроса. Это означает, что второй запрос снова получит данные конфигурации и попадет на удаленный сервер, а это не то, что нам нужно.

На самом деле проблема формально называется **Cache Stampede (Breakdown)** и определение выглядит следующим образом:

Давление кеша происходит, когда к кешу одновременно обращается большое количество клиентов, что приводит к отправке большого количества запросов на исходный сервер. Это может произойти, если кеш признан недействительным или если кеш заполнен неправильно.

Решение

Решение состоит в том, чтобы заставить все запросы ждать, пока данные конфигурации не будут получены и кэшированы. Мы можем сделать это, кэшируя промис, который разрешается при получении данных конфигурации. Давайте назовем его configPromise и дадим ему следующую реализацию:

let configPromise;
async function fetchCachedConfig() {
   if (configPromise) {
       return configPromise;
   }
   configPromise = fetchConfig();
   return configPromise;
}

И снова запускаем тесты, все зеленое, Ура🎉🎉🎉

PASS ./index.test.js
✓ Should be called only once when several requests fired sequentially (2 ms)
✓ Should be called only once when several requests fired concurrently (1 ms)

Заключение

В этой статье мы рассмотрели, как правильно кэшировать удаленно полученные данные в Node.js, чтобы избежать давки в кэше. Кэшируя обещание, которое разрешается при извлечении данных, мы можем гарантировать, что все запросы будут ожидать кэширования данных, прежде чем продолжить, предотвращая попадание нескольких запросов на удаленный сервер.