Ответ
При возникновении deadlock'а или для их предотвращения в PHP-приложениях я применяю комбинацию стратегий на уровне приложения и SQL.
1. Оптимизация транзакций и запросов:
- Удерживаю блокировки минимальное время: Выполняю всю необходимую бизнес-логику до открытия транзакции, а внутри транзакции — только операции с БД.
- Выполняю запросы в постоянном порядке: Например, всегда сначала блокирую запись с меньшим ID. Это предотвращает циклические зависимости.
- Использую пессимистичные блокировки осознанно:
$pdo->beginTransaction(); // SELECT ... FOR UPDATE блокирует строки для изменения $stmt = $pdo->prepare('SELECT * FROM invoices WHERE id = ? FOR UPDATE'); $stmt->execute([$invoiceId]); // ... операции обновления ... $pdo->commit();
2. Настройка таймаутов и повторов:
- Устанавливаю короткий таймаут для ожидания блокировки, чтобы запрос не «висел» вечно.
SET LOCK_TIMEOUT 2000; -- Таймаут 2 секунды для MySQL - В коде приложения реализую механизм повторных попыток (retry logic) с экспоненциальной задержкой, если транзакция откатилась из-за deadlock.
$maxRetries = 3; for ($attempt = 0; $attempt < $maxRetries; $attempt++) { try { // ... выполнение транзакции ... break; // Успех, выходим из цикла } catch (PDOException $e) { if ($attempt === $maxRetries - 1 || strpos($e->getMessage(), 'Deadlock') === false) { throw $e; // Если это не deadlock или попытки кончились — пробрасываем исключение } usleep(100000 * pow(2, $attempt)); // Экспоненциальная задержка } }
3. Альтернативные подходы:
- Оптимистичная блокировка: Использую поле
version(или timestamp) в таблице. Перед обновлением проверяю, что версия не изменилась. - Очереди задач: Для длительных или конфликтующих операций ставлю задачу в очередь (RabbitMQ, Redis). Обработка происходит последовательно в одном воркере, что исключает конкуренцию.
- Выбор уровня изоляции: В некоторых случаях помогает снижение уровня изоляции транзакции (например, до
READ COMMITTED), чтобы уменьшить количество блокировок.