Мьютексные блокировки в php фреймворках
Решил я разобраться как устроены мьютексы в различных фреймворках. А началось всё с того, что произошёл у меня спор с коллегой по поводу того, как отработает код в Yii2:
$mutex->acquire('my-lock', 10);
Поскольку в нашем проекте мьютексы хранятся в Redis, я был уверен, что число 10
в этом контексте обозначает TTL (Time To Live) блокировки, то есть время, через которое блокировка будет автоматически снята. Это предположение основывалось на том, что в команде Redis для установки блокировки используется следующий синтаксис:
SET lock_name unique_value NX PX 5000
Здесь создаётся блокировка с именем lock_name
, где "владельцем" является unique_value
(снять блокировку можно только, указав это значение) на 5000 миллисекунд, если такого ключа у "владельца" нет.
Кроме того, мой опыт работы с Symfony подсказывал, что при создании мьютекса можно передать только TTL. Однако, как оказалось, в случае с Yii2 я ошибался. Изучив код, я понял, что параметр 10
в методе acquire
обозначает не TTL, а таймаут — время, в течение которого процесс будет ожидать освобождения мьютекса.
Как устроены мьютексы в PHP-фреймворках?
Рассмотрим интерфейсы и их реализацию в различных фреймворках, поскольку на глубоком уровне они работают схожим образом, особенно при использовании Redis.
Yii2
if (!$mutex->acquire($mutexName)) { return; } try { // критическая секция } finally { $mutex->release(); }
При таком использовании метод acquire
сразу вернёт false
, если блокировка не может быть установлена, и ожидание не произойдёт. Если вызвать $mutex->acquire($mutexName, 10)
, процесс будет ожидать до 10 секунд, пока мьютекс не освободится. Однако TTL также можно задать.
Yii3
В Yii3 добавлена обёртка \Yiisoft\Mutex\Synchronizer
:
$synchronizer = new Synchronizer($mutexFactory); $result = $synchronizer->execute('my-lock', function () { // критическая секция }, 10); // таймаут 10 секунд
Здесь освобождение мьютекса происходит внутри Synchronizer
, что удобнее и безопаснее. Это избавляет от необходимости писать собственные обёртки. Для тех, кто предпочитает полный контроль, мьютексы можно использовать так же, как в Yii2.
Laravel
В Laravel мьютексы являются частью пакета кеширования:
Cache::lock('my-critical-section', 10)->block(5, function () { // критическая секция });
Здесь создаётся мьютекс с TTL 10 секунд и таймаутом 5 секунд. Освобождение происходит автоматически. Можно также выполнить всё вручную:
$lock = Cache::lock('my-task', 10); if ($lock->get()) { try { // критическая секция } finally { $lock->release(); } }
Мьютексные блокировки часто применяются для предотвращения параллельного запуска фоновых задач. В Laravel существует middleware WithoutOverlapping
в пакете очередей, который использует тот же мьютекс, предоставляя удобную обёртку для типовых задач.
Symfony
Symfony предоставляет, пожалуй, самый гибкий и полный пакет для работы с мьютексами:
$lock = $factory->createLock('pdf-creation', ttl: 30); if (!$lock->acquire()) { return; } try { // выполнение задачи менее 30 секунд } finally { $lock->release(); }
Здесь блокировка создаётся в неблокирующем режиме. Можно создать и в блокирующем, используя acquire(true)
. Также можно обновлять TTL мьютекса через $lock->refresh()
, если становится понятно, что первоначального TTL недостаточно.
Особое внимание в этом пакете привлекают блокировки с разделением доступа. Поддержка этой функции зависит от используемого адаптера; для Redis она доступна. В то время как acquire
захватывает исключительную блокировку, после которой никакой другой процесс не сможет ни читать, ни писать в защищённый ресурс, acquireRead
(разделяемая блокировка) позволяет нескольким процессам захватывать блокировку в режиме чтения, пока кто-то не выполнит исключительную блокировку.
Подобные библиотеки делают использование мьютексов простым и удобным, скрывая тонкости реализации. Например, последовательное выполнение команд GET
(проверка наличия ключа) и DEL
(удаление ключа) для снятия мьютекса не гарантирует атомарность операции, поскольку между выполнением этих команд значение ключа может измениться другим процессом. Поэтому требуется выполнение Lua-скрипта:
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end
Этот скрипт обеспечивает атомарность операции удаления мьютекса, проверяя значение ключа перед удалением.
Подписывайся на канал 🤘