Эта проблема

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

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

Кажется, достаточно просто. Однако суровая реальность асинхронной природы node.js и цикла обработки событий преподносит нам неприятный сюрприз. Если мы отправим два параллельных запроса, произойдет следующее:

Req 1: await get("aKey")
Req 2: await get("aKey")
Req 1: cache miss
Req 1: await for slowAsyncOperation()
Req 2: cache miss
Req 2: await for slowAsyncOperation()
Req 1: await set("aKey", value)
Req 2: await set("aKey", value)

Проклятие! Каждый параллельный запрос будет пытаться получить значение и сохранить его. Это плохо. Нам нужно синхронизировать ветвь промаха кэша и предотвратить множественные запросы на выполнение кода в «одно и то же» время. Нам нужно взаимное исключение.

Mutex спешит на помощь

взаимное исключение - свойство контроля параллелизма, которое устанавливается с целью предотвращения состояний гонки; это требование, чтобы один поток выполнения никогда не входил в свою критическую секцию, в то время как другой параллельный поток выполнения входит в свою собственную критическую секцию.

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

Есть несколько библиотек npm, которые реализуют какую-то поддержку мьютексов (lock, mutex, async-lock), но ни одна из них не кажется такой простой и элегантной, как код Валери. Его решение (вставлено ниже с оригинальными комментариями):

  • Использует логическое значение для сохранения заблокированного / разблокированного состояния мьютекса.
  • Использует обещания дождаться снятия блокировки.
  • Опирается на эмиттер событий для уведомления кода, который ожидает, когда блокировка будет снята.

Ниже представлена ​​первая рабочая версия fetch, которая использует эту «блокировку», чтобы гарантировать выполнение только одного set.

Улучшение кода

Код по-прежнему вызывает некоторые проблемы из-за Lock общего назначения:

  • Все записи синхронизированы. Лучше всего заблокировать ключом кеша, чтобы максимизировать безопасную параллельную запись.
  • После входа в мьютекс требуется дополнительный get вызов, чтобы избежать повторных set вызовов. Мы бы хотели избежать повторного отключения сети, если это возможно.

Мы решили изменить исходный код блокировки, чтобы сделать специальную блокировку «выборки», адаптированную к нашей проблеме:

  • Вместо логического флага мы используем карту, чтобы разрешить пометку по ключу кеша.
  • Обещание мьютекса теперь преобразуется в новое полученное значение, чтобы избежать второй операции get (мы передаем новое значение через EventEmmiter).

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

С этой новой «блокировкой выборки» окончательный fetch код выглядит следующим образом:

Подводя итоги

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