В недавнем проекте, над которым я работал, возникла проблема, вызванная нехваткой памяти для процесса. Позвольте мне провести вас через некоторые недоразумения, проясненные благодаря большому количеству головных уборов и командной работе.
Предположим, у вас есть служба, которая загружает данные из кеша, но при отсутствии результата возвращается к базе данных.
Наш первый подход может выглядеть примерно так:
Похоже, мы рассмотрели то, что нам нужно сделать, и если мы запускаем последовательные запросы — так оно и есть. Проблема начинается, когда мы вызываем функцию одновременно более одного раза:
Тогда наш вывод может выглядеть примерно так:
Data retrieved from database: nugget Data retrieved from database: nugget Data retrieved from database: hunk Data retrieved from database: mass { results: [ 'nugget', 'nugget', 'hunk', 'mass' ] }
Почему мы не попали в кеш для идентификатора, который мы уже запрашивали в базе данных?
В service.js
все наши запросы приходят одновременно, и в этот начальный момент кэш еще не заполнен, это означает, что мы попадаем в оператор catch, где вместо этого данные извлекаются из базы данных.
Итак, следующее, что мы могли бы попробовать, — реализовать систему очередей для дедупликации запросов:
Если мы снова запустим consumer.js
, мы получим:
Data retrieved from database: nugget Data retrieved from database: nugget Data retrieved from database: hunk Data retrieved from database: mass { results: [ 'nugget', 'nugget', 'hunk', 'mass' ] }
Тот же результат. Почему?
В dedupe.js
блоках await workToComplete
до того, как queue[id]
успеет застыть. Приходит следующий вызов, и в итоге повторяется то же условие, что приводит к промаху кеша.
Как сделать так, чтобы все последующие запросы ждали после первого?
Блин, это начинает усложняться.
При первом поступлении запроса работа выполняется и возвращается ссылка на промис. Выполнение активных блоков потока по ключевому слову await
.
Когда поступает другой запрос с тем же идентификатором, возвращается первый промис, который позже выполняется с результатом первого.
Вывод при запуске consumer.js
теперь выглядит так:
Performing work: nugget Waiting for previous work to complete: nugget Performing work: hunk Performing work: mass Data retrieved from database: nugget Data retrieved from database: hunk Work completed: nugget Data retrieved from database: mass Work completed: hunk Work completed: mass { results: [ 'nugget', 'nugget', 'hunk', 'mass' ] }
Мы видим, что работа выполняется (извлечение данных из базы данных) один раз для каждого идентификатора (т. е. самородка), а последующие запросы ждут завершения предыдущего (кэш в памяти).
Если мы снова запустим consumer.js
, то увидим, что попадаем в кеш при выполнении работы и в кеш в памяти при последующих вызовах:
Performing work: nugget Waiting for previous work to complete: nugget Performing work: hunk Performing work: mass Data retrieved from cache: nugget Work completed: nugget Data retrieved from cache: hunk Work completed: hunk Data retrieved from cache: mass Work completed: mass { results: [ 'nugget', 'nugget', 'hunk', 'mass' ] }
Функционально это дает нам то, что мы намеревались сделать, однако мы можем улучшить это дальше:
Используя Map
, мы удаляем кучу null
значений, которые создаются, когда обещание выполнено или отклонено, и перемещаем код, чтобы мы могли вернуться раньше, это делает код более сжатым и немного более легким для понимания.
Спасибо следующим людям: Виктории Фивер, Оливеру Рамбелоу, Люку Джонсу.