Как исправить ситуацию с блокировкой (deadlock) в базе данных?

«Как исправить ситуацию с блокировкой (deadlock) в базе данных?» — вопрос из категории Базы данных, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

При возникновении 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), чтобы уменьшить количество блокировок.