Вступление

Когда два или более клиентов хотят обновить одну и ту же запись, может возникнуть конфликт, известный как состояние гонки. Чтобы предотвратить такой конфликт, система пессимистов предполагает худшее, т. Е. Обновления всегда происходят в одно и то же время. Таким образом, он устраняет состояние гонки, блокируя запись. Пессимистические системы обычно полагаются на средства блокировки базы данных; например, блокировка на уровне строк InnoDB.

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

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

Некоторые фреймворки имеют встроенную поддержку обоих механизмов. Например, с Rails и его инфраструктурой ORM, Active Record, можно выбирать между Locking::Optimistic и Locking::Pessimistic. Однако в Laravel, который используется в этой статье и на который сильно повлиял Rails, не хватает такой возможности ». Поэтому мы также представили здесь простую реализацию оптимистической блокировки2.

заявка

Чтобы сравнить оба механизма, мы рассмотрим простое приложение: банковская система состоит из нескольких учетных записей, и каждая учетная запись имеет баланс. Предполагается, что система предоставляет API под названием transfer, который переводит заданную сумму с одной учетной записи на другую. Чтобы предотвратить условия гонки, которые могут привести к переводу недостаточного количества средств или потере переведенной суммы на принимающем счете, приложение должно использовать механизм блокировки.

Пессимистическую блокировку легко реализовать с помощью транзакции БД, как показано ниже. В Laravel все операции с БД между beginTransaction и commit гарантированно выполняются атомарно.

Кроме того, оптимистическая блокировка реализуется, как показано ниже. Обратите внимание, что запросы SELECT и INSERT являются операциями чтения / записи, которые, как правило, не требуют блокировок. В этом фрагменте кода волшебство происходит внутри while циклов. Все учетные записи имеют updated_at метку времени, которая автоматически обновляется, когда запись появляется в UPDATE запросе. На каждой итерации цикл гарантирует, что запись не обновляется между последовательными SELECT и UPDATE, сужая обновление с помощью предложения WHERE в столбце updated_at. При обнаружении вторжения (т. Е. Обновления не происходит) выполняется повторная попытка всего процесса. (Примечание: этот фрагмент не учитывает максимальное количество итераций, что является плохой практикой, и не реализует откат. Однако эти функции можно реализовать с помощью немного сложного алгоритма.)

Контрольный показатель

Обе реализации выполняются с использованием valet³ и базы данных MySQL. Затем они подвергаются тестированию с использованием саранчи с тем же исходным набором данных на той же машине⁵.

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

При этом существует проблема, заключающаяся в том, что передачи выполняются так быстро, что остается лишь шанс возникновения условий гонки. Итак, чтобы блокировки сохранялись дольше, мы вводим в код команду sleep(1), чтобы заставить транзакцию удерживаться в течение одной секунды. Теперь результаты тестов менее очевидны:

  • оптимистичный подход дает около 6 запросов в секунду, при этом 11% запросов терпят неудачу.
  • пессимистический подход приводит к примерно 3 RPS, при этом 45% запросов не выполняются.

Чтобы быть кратким, эталонный тест показывает на 100% большую эффективность для оптимистической блокировки, чем для пессимистической, и запросы с меньшей вероятностью потерпят неудачу.

Важно выяснить причину сбоев. Результаты показывают, что при оптимистической блокировке все неудавшиеся запросы отклоняются Nginx (т. Е. Для запроса не было доступных php-fpm процессов). С другой стороны, для пессимистической блокировки около двух третей сбоев вызваны сообщением MySQL об ошибке lock-wait (т. Е. Попыткой заблокировать уже заблокированную строку).

РЕЗЮМЕ

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

Следующий шаг

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

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

¹ Эта реализация предназначена только для целей тестирования и не предназначена для использования в производственной среде. Исходный код доступен бесплатно с github.

² Координаторы Laravel отклонили предложения об оптимистичной поддержке блокировок. Однако его можно поддерживать с помощью сторонних библиотек.

³ Valet использует Nginx и php-fpm и другие вещи для запуска приложения Laravel.

⁴ Мы использовали набор всего из 10 учетных записей, чтобы сделать условия гонки более вероятными.

⁵ MacBook Pro, процессор Core i5, установленная оперативная память 8 ГБ, жесткий диск SSD