Если несколько PHP-воркеров могут одновременно выполнять задачу над одним и тем же ресурсом, блокировка помогает сократить объём дублирующей работы. Обычно её используют при создании отчёта, обработке заказа или обновлении общего ресурса. Однако одно лишь использование Redis не делает операцию согласованной: процесс может приостановиться, срок действия блокировки может истечь, и другой воркер начнёт работу до завершения первого.
Важно не только то, как создать ключ, но и то, какое свойство нужно защитить и какая система может обеспечить его соблюдение. Во многих случаях более прямую гарантию даёт ограничение уникальности или транзакция базы данных. Распределённые блокировки с Redis в PHP полезны, когда координируют процессы, которые нельзя согласовать одной атомарной операцией в базе данных, если при этом рассматривать блокировку как временную защиту, а не как окончательный источник истины.
Что решает распределённая блокировка и чего она не гарантирует

Распределённая блокировка позволяет нескольким процессам, возможно работающим на разных серверах, конкурировать за временный эксклюзивный доступ. Если воркер захватил блокировку, остальные не должны входить в критическую секцию, пока она действует. Это полезно, например, чтобы не допустить одновременного запуска двумя процессами дорогостоящей задачи или скоординировать работу, затрагивающую внешние сервисы.
Сама по себе блокировка не гарантирует, что будет выполнена только одна запись, что процесс-владелец всё ещё работает или что Redis и база данных сохраняют согласованное состояние. Это временная аренда, то есть lease. Если воркер остановится дольше, чем действует блокировка, Redis может разрешить захватить её другому процессу, хотя первый продолжит работу после возобновления.
Поэтому приложению не следует полагаться только на ключ Redis при защите бизнес-правила. У конечной операции должны быть собственные механизмы защиты: проверки, ограничения целостности, контроль версий или идемпотентность — в зависимости от ситуации.
Когда использовать Redis, а когда выбрать базу данных
Прежде чем добавлять блокировку, определите условие, которое нужно сохранить. Если задача состоит в том, чтобы у сущности не было двух записей с одним идентификатором, ограничение уникальности в базе данных явно выражает это правило и обеспечивает его соблюдение в месте записи данных. Оно не зависит от того, соблюдают ли все воркеры внешний протокол.
Если несколько изменений должны фиксироваться вместе, может быть достаточно транзакции. Чтобы не перезаписать параллельное обновление, подойдёт оптимистическая блокировка: прочитать версию, обновить данные, только если она не изменилась, и явно обработать конфликт. Для операций со строками или наборами данных может быть уместнее пессимистическая блокировка на уровне самой базы данных, хотя её область действия и продолжительность нужно ограничивать.
Redis может быть хорошим выбором, если блокировка координирует работу между процессами и нет одной операции базы данных, которая решала бы эту задачу, или если нужно не допустить одновременного запуска повторяющейся задачи множеством воркеров. Также оцените, оправданы ли затраты на ожидание, повторные попытки и работу с Redis. Если проблему проще решить ключом с ограничением уникальности и меньшим количеством компонентов, обычно предпочтительнее этот вариант.
Атомарный захват, уникальный владелец и срок действия
Захват должен выполняться одной атомарной операцией: создать ключ, только если его ещё нет, и в той же операции назначить ему срок действия. В Redis для этого обычно используют SET с параметрами, эквивалентными «только если ключ не существует», и временем жизни. Раздельное создание ключа и назначение срока действия создаёт гонку: процесс может завершиться с ошибкой между этими действиями и оставить блокировку без срока истечения.
Значением ключа должен быть уникальный токен, сгенерированный для этой попытки, а не постоянное значение, общее для всех воркеров. Токен позволяет определить владельца и не даёт процессу случайно удалить блокировку, которую после него захватил другой процесс. PHP-клиент должен считать блокировку захваченной, только если Redis подтвердил успешное выполнение условной операции.
// Концептуальная схема; адаптируйте вызов к используемой библиотеке Redis.
$token = generarTokenUnico();
$adquirido = redisSetSiNoExisteConExpiracion($clave, $token, $duracion);
if (!$adquirido) {
// Не входить в критическую секцию.
}
Срок действия должен превышать ожидаемую продолжительность работы, но не быть настолько большим, чтобы после сбоя другие процессы неоправданно долго ждали. Универсального значения нет: измерьте фактическую длительность, учтите её вариации и определите, что делать с задачами, приближающимися к установленному пределу. Продление аренды также требует проверки, что токен всё ещё принадлежит этому процессу; безусловное продление может увеличить срок действия блокировки другого воркера.
Безопасное освобождение и риски истечения срока действия
Не удаляйте ключ простым вызовом удаления после завершения работы. Если исходная блокировка истекла и другой процесс захватил новую, первый воркер может удалить ключ второго. При освобождении нужно сравнить сохранённое значение с токеном владельца и удалить ключ, только если они совпадают, выполнив это атомарно. Для этого можно запустить скрипт в Redis или использовать эквивалентный примитив, который сравнивает значения и удаляет ключ без промежуточных операций.
Даже при правильном захвате и освобождении длительная приостановка процесса может привести к параллельной записи. Воркер A захватывает блокировку, приостанавливается, срок его lease истекает; воркер B захватывает блокировку и выполняет запись; затем A возобновляет работу и тоже выполняет запись. Ещё хуже, если из-за сетевого разделения или сбоя соединения клиент не знает, была ли выполнена операция захвата.
Если запоздалая запись может привести к ущербу, защищаемый ресурс должен отклонять её независимо от блокировки. Одна из стратегий — использовать возрастающий fencing token или номер версии и требовать от системы, принимающей запись, отбрасывать устаревшие значения. Такой счётчик должен обладать необходимыми для конкретного случая свойствами упорядочивания и долговечности; нельзя автоматически считать счётчик Redis гарантией, не учитывая перезапуски или failover. Другой вариант — проверять версию в базе данных непосредственно в операции записи.
Критическая секция, конкуренция и ошибки при захвате
Оставляйте внутри блокировки только работу, которой действительно необходим эксклюзивный доступ. Не включайте туда медленные вызовы внешних сервисов, ожидание пользователя или независимую обработку. Чем дольше длится критическая секция, тем выше конкуренция и вероятность превысить срок действия блокировки. Если работу можно разделить, отделите краткую координацию от задач, не зависящих от общего состояния.
Если захватить блокировку не удалось, не следует повторять попытки бесконечно или надолго занимать PHP-воркеры ожиданием. Выберите политику с учётом важности задачи: отказаться от выполнения и записать конфликт в журнал, ограниченно повторять попытки с задержкой или отправить задачу в очередь для последующей обработки. Повторный запуск должен быть безопасным и идемпотентным, а система должна отличать обычную конкуренцию за блокировку от сбоя Redis.
Если состояние соединения неопределённо, не считайте заранее, что блокировка была или не была захвачена. Спроектируйте поток выполнения так, чтобы он мог восстановиться без повторного выполнения необратимого действия: например, используйте ключи идемпотентности для бизнес-операции и проверяйте её результат перед повтором.
Тестирование и операционные показатели
Проверяйте поведение с несколькими воркерами и контролируйте порядок чередования операций, а не только штатный сценарий. Включите случаи, когда процесс завершается до освобождения блокировки, срок действия истекает во время работы, Redis не отвечает, а старый воркер пытается освободить блокировку после того, как её захватил другой. Убедитесь, что токен не позволяет удалить чужой ключ, а запоздалые записи отклоняются, если приложению нужна такая гарантия.
В production отслеживайте неудачные попытки захвата, длительность критических секций, истечения срока действия, продления, ошибки соединения и повторные попытки. Рост конкуренции может указывать на слишком широкий охват блокировки или на то, что внутри неё выполняется лишняя работа. Частое истечение срока действия при обычной работе означает, что нужно пересмотреть срок аренды или дизайн процесса, а не просто автоматически увеличить TTL.
Контрольный список для принятия решения

- Какое бизнес-правило защищается и можно ли выразить его ограничением или транзакцией в базе данных?
- Выполняется ли захват атомарно и условно, с назначением срока действия?
- Используется ли для каждой попытки уникальный токен и сравнивается ли владелец перед удалением при освобождении?
- Что произойдёт, если процесс будет приостановлен дольше, чем действует lease, или Redis потеряет соединение?
- Может ли система, принимающая запись, отклонять устаревшие версии или дубликаты?
- Есть ли ограниченная политика ожидания, повторных попыток и восстановления, а также метрики и тесты конкурентного доступа?
Redis обеспечивает временную координацию, а не универсальную гарантию согласованности. Используйте его, когда задача действительно требует координировать распределённые процессы, и сохраняйте окончательную защиту в системе, записывающей данные. Если правило проще обеспечить на уровне базы данных, начните с этого.



