#Что означает неисправность
ERROR 1205 (HY000) «Lock wait timeout exceeded; try restarting transaction» в обработчике оплаты указывает, что запрос слишком долго ожидал блокировку строки InnoDB. Важно: при стандартной конфигурации MySQL откатывает только неудавшийся SQL-оператор, а не всю транзакцию. В платёжной системе нельзя бездумно повторять списание: нужен явный ROLLBACK, фиксация состояния и устойчивый ключ идемпотентности.
#Возможные причины и признаки
| Возможная причина | Как узнать | Что делать |
|---|---|---|
| Два обработчика одновременно меняют один заказОба обработчика блокируют одну и ту же строку заказа или баланса. | Ошибка 1205 совпадает по времени с двумя запросами на один order_id. | Сократить блокировки и сериализовать изменения по одному идентификатору. |
| Транзакция держит блокировки во время внешнего HTTP-запросаСеть платёжного провайдера может отвечать долго, пока строка заказа остаётся заблокирована. | В журнале долгая транзакция без активного SQL, но другие запросы ждут. | Перенести внешний вызов за пределы короткой транзакции и использовать состояния операции. |
| Код повторяет платёж без идемпотентностиПосле тайм-аута приложение снова обращается к провайдеру с новым ключом. | У клиента два платежа или несколько попыток с разными внешними идентификаторами. | Повторять только безопасную операцию после проверки статуса платежа у провайдера. |
#Пошаговая диагностика и решение
- Остановите автоматическое повторение списания до выяснения статусаЕсли клиент видит ошибку оплаты, не предлагайте ему сразу нажимать «Оплатить» ещё раз и не перезапускайте платёжный вызов с новым идентификатором. Сохраните внутренний order_id, внешний payment_id и idempotency key из закрытого журнала. Проверка нужна именно для конкретного заказа; персональные данные и платёжные токены в открытом журнале не сохраняйте.
- Различите ошибку MySQL и результат у платёжного провайдераСверьте в журнале клиента, была ли ошибка 1205 до отправки запроса провайдеру или после его ответа. Через служебный интерфейс провайдера проверьте фактический статус платежа по внешнему идентификатору. Если деньги уже списаны, нельзя создавать новое списание из-за неуспешного UPDATE локальной базы; выполняйте согласованную сверку данных и исправление статуса заказа.
- Найдите границы SQL-транзакции в кодеВ репозитории найдите BEGIN/START TRANSACTION, первый SELECT FOR UPDATE, UPDATE статуса, COMMIT и все ветки ошибок. Убедитесь, что обработчик при любой ошибке, включая 1205, выполняет ROLLBACK в текущем соединении. По умолчанию MySQL при ожидании блокировки откатывает только проблемный оператор, остальные блокировки этой транзакции могут сохраняться до явного отката.
- Просмотрите ожидания блокировок без прерывания соединенийНа MySQL 8.0/8.4 в SQL-редакторе с правами администратора можно читать
performance_schema.data_lock_waitsиdata_locks; их структура и права зависят от версии. Если таблицы доступны, сопоставьте ожидающие и блокирующие транзакции с временем заказа. Не завершайте чужие запросы через KILL автоматически: это может прервать оформление заказа другого покупателя. - Уберите сетевые вызовы и тяжёлые операции из критической транзакцииНайдите участок, где под блокировкой заказа выполняются внешние HTTP-запросы, отправка писем, журналирование в медленный сервис или расчёт большого отчёта. Перестройте процесс на короткую транзакцию изменения состояния, затем отдельный идемпотентный вызов провайдера и последующую сверку. Такой переход требует проектирования состояния pending/succeeded/failed и тестов отказов, не делайте его одиночным правкой SQL.
- Повторяйте только безопасную локальную транзакциюЕсли локальная транзакция получила 1205, выполните ROLLBACK и, если бизнес-операция допускает повтор, создайте новую транзакцию, повторно прочитайте актуальное состояние заказа и примите решение. Ограничьте число попыток, добавьте паузу и зарегистрируйте каждую попытку. Сам внешний платёж не вызывайте повторно без устойчивого ключа и правил API провайдера; результат предыдущего запроса мог быть успешным.
- Закрепите защиту от двойного выполнения на уровне данныхВ проекте согласуйте UNIQUE по внешнему payment_id или по идентификатору платежной операции (с учётом модели нескольких попыток на заказ). Проверьте, что обработчик повторного уведомления провайдера узнаёт уже зафиксированную операцию и возвращает корректный ответ без нового зачисления. Установка UNIQUE требует поиска существующих дублей и безопасной миграции; удалять дубли автоматически нельзя.
- Проверьте параллельные запросы на отдельном стендеНа тестовой базе с фиктивными заказами создайте два соединения, чтобы один UPDATE держал блокировку, а второй ждал её и получил 1205 при коротком SESSION lock_wait_timeout. После этого явный ROLLBACK на обоих соединениях обязателен. Проверьте, что обработчик оставил заказ в согласованном состоянии, а повторное уведомление не изменило сумму второй раз. Не воспроизводите это на реальных платежах.
- Проверьте восстановление после сбоев и согласуйте откатВ тестовой системе проверьте ситуации: тайм-аут до вызова провайдера, тайм-аут после успешного ответа и разрыв связи с неизвестным итогом. Для каждой должна существовать безопасная процедура сверки по неизменному ключу платежа. Если итоги в локальной БД и у провайдера расходятся, приостановите автоматический повтор и передайте случай на ручную сверку. Откат кода делайте без удаления истории платежей.
#Команды для диагностики и проверки
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';SHOW VARIABLES LIKE 'innodb_rollback_on_timeout';SELECT * FROM performance_schema.data_lock_waits LIMIT 20;#Пример для отдельной тестовой базы или учебного проекта
CREATE TEMPORARY TABLE whelp_payments (id INT PRIMARY KEY, order_id INT NOT NULL, status VARCHAR(20), UNIQUE KEY uq_order (order_id)) ENGINE=InnoDB;
INSERT INTO whelp_payments VALUES (1, 1001, 'pending');
START TRANSACTION;
UPDATE whelp_payments SET status='paid' WHERE order_id=1001;
ROLLBACK;
SELECT status FROM whelp_payments WHERE order_id=1001;#Проверка результата и дальнейшие действия
Успех подтверждается не исчезновением одной ошибки SQL, а сохранением финансовых инвариантов: один внешний платеж отражён одной финансовой операцией, сумма не удвоена, а повторный веб-запрос или уведомление не вызывает второго списания. Если 1205 возникает вновь, соберите сведения о блокирующих транзакциях, длительности их удержания и сценариях восстановления. При расхождении статуса банка и локальной БД сначала выполняйте сверку, а не новый платёж.