При разработке серверных приложений в 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, чтобы избежать давки в кэше. Кэшируя обещание, которое разрешается при извлечении данных, мы можем гарантировать, что все запросы будут ожидать кэширования данных, прежде чем продолжить, предотвращая попадание нескольких запросов на удаленный сервер.