Разрешение взаимоблокировок MySQL: стратегии и лучшие практики
Диагностируйте взаимоблокировки MySQL, уменьшайте конфликты блокировок и добавляйте безопасную логику повторных попыток для транзакций, которые можно повторить.
Разрешение взаимоблокировок MySQL: стратегии и лучшие практики
Взаимоблокировки MySQL возникают, когда транзакции блокируют друг друга в цикле. InnoDB обнаруживает цикл и откатывает одну транзакцию, чтобы другая могла продолжить. Этот автоматический откат полезен, но не делает проблему безвредной. Приложение все равно видит ошибку, действие пользователя может завершиться неудачей, а повторяющиеся взаимоблокировки могут превратить обычный всплеск трафика в инцидент поддержки.
Одиночная взаимоблокировка время от времени не является чем-то необычным в загруженной транзакционной системе. Постоянный поток взаимоблокировок — это сигнал. Обычно это означает, что транзакции слишком широкие, строки блокируются в несогласованном порядке или запросы сканируют больше данных, чем необходимо.
Понимание взаимоблокировок MySQL
В современной работе с MySQL устранение взаимоблокировок обычно означает InnoDB, потому что это стандартный транзакционный движок, использующий блокировки строк, индексов, промежутков и next-key. Другие движки могут блокировать, тайм-аутить или сериализовать записи по-другому, но InnoDB — это место, где происходит большинство инцидентов взаимоблокировок приложений.
Цикл взаимоблокировки
Взаимоблокировка обычно следует этому шаблону:
- Транзакция A получает блокировку на ресурс X.
- Транзакция B получает блокировку на ресурс Y.
- Транзакция A пытается получить блокировку на ресурс Y, но вынуждена ждать, потому что B удерживает его.
- Транзакция B пытается получить блокировку на ресурс X, но вынуждена ждать, потому что A удерживает его.
На этом этапе ни одна транзакция не может продолжить. InnoDB обнаруживает этот цикл ожидания и откатывает одну транзакцию, часто называемую жертвой взаимоблокировки. Приложение обычно получает код ошибки SQL 1213 (ER_LOCK_DEADLOCK). Вся транзакция теряется, а не только последний оператор, поэтому логика повторных попыток должна запускать транзакцию с самого начала.
Распространенные причины взаимоблокировок
Взаимоблокировки обычно возникают из-за плохого проектирования транзакций или неэффективных запросов:
- Длительные транзакции: Транзакции, которые удерживают блокировки в течение длительного времени, значительно увеличивают вероятность коллизии.
- Несогласованный порядок операций: Две транзакции обновляют один и тот же набор строк или таблиц, но в разной последовательности.
- Отсутствие или неэффективность индексов: Когда индексы отсутствуют или недостаточно селективны, InnoDB может проверять и блокировать гораздо больше записей индекса или диапазонов, чем ожидало приложение, увеличивая площадь блокировки.
- Высокая степень параллелизма: Естественно, интенсивные одновременные записи в одни и те же наборы данных увеличивают вероятность коллизии.
Диагностика и анализ взаимоблокировок
Когда происходит взаимоблокировка, первый шаг — определить участвующие транзакции и конкретные блокировки, которые они удерживали. Основной диагностический инструмент в MySQL — SHOW ENGINE INNODB STATUS.
Использование SHOW ENGINE INNODB STATUS
Выполните следующую команду и изучите вывод, обращая особое внимание на раздел LATEST DETECTED DEADLOCK.
SHOW ENGINE INNODB STATUS\G
Вывод LATEST DETECTED DEADLOCK предоставляет важные криминалистические данные, подробно описывая:
- Участвующие транзакции (ID, состояние и продолжительность).
- Оператор SQL, который выполняла жертва в момент возникновения взаимоблокировки.
- Конкретную строку и индекс, которые ожидались.
- Ресурсы, удерживаемые блокирующей транзакцией.
Совет: Инструменты анализа журналов могут автоматически извлекать и классифицировать эти записи взаимоблокировок, которые также часто записываются в журнал ошибок MySQL.
Для повторяющихся инцидентов собирайте более одного образца. SHOW ENGINE INNODB STATUS показывает последнюю обнаруженную взаимоблокировку, поэтому новая взаимоблокировка может перезаписать предыдущие доказательства. В системах, где взаимоблокировки часты, включите ведение журнала взаимоблокировок в журнал ошибок, если ваша версия MySQL и операционная политика это позволяют:
SET GLOBAL innodb_print_all_deadlocks = ON;
Не оставляйте дополнительное ведение журнала включенным без учета объема. Это полезно во время инцидента, но шумные системы могут генерировать много данных журнала.
Когда вы читаете отчет о взаимоблокировке, ищите три вещи, прежде чем менять код:
- Какие операторы SQL были задействованы?
- Какие индексы использовались?
- Блокировали ли транзакции одни и те же ресурсы в разном порядке?
Последний вопрос обычно самый показательный. Если один путь кода обновляет accounts, а затем ledger_entries, в то время как другой обновляет ledger_entries, а затем accounts, исправление, вероятно, не в настройках сервера. Исправление заключается в согласовании одного порядка.
Один практический трюк — вставить две транзакции в заметку и отметить каждый оператор, который может блокировать строки: UPDATE, DELETE, INSERT в таблицы с уникальными ключами, SELECT ... FOR UPDATE и некоторые проверки внешних ключей. Взаимоблокировки часто становятся очевидными, когда вы перестаете смотреть на один неудачный запрос и смотрите на полную последовательность вокруг него.
Стратегия предотвращения 1: Оптимизация транзакций
Самый эффективный способ предотвратить взаимоблокировки — сократить время удержания блокировок и стандартизировать доступ к ресурсам.
1. Делайте транзакции короткими и атомарными
Транзакция должна включать только абсолютно необходимые операции. Чем дольше выполняется транзакция, тем дольше она удерживает блокировки и тем выше вероятность коллизии.
- Плохая практика: Извлечение данных, выполнение сложной бизнес-логики на уровне приложения, а затем обновление данных, все в рамках одной длинной транзакции.
- Лучшая практика: Выполняйте бизнес-логику вне транзакции. Транзакция должна включать только шаги
SELECT FOR UPDATE, обновление/вставку иCOMMIT.
Например, не открывайте транзакцию, а затем вызывайте платежного провайдера, отправляйте электронное письмо или ждите удаленного API. Эти операции могут занять сотни миллисекунд или секунд, и в течение этого времени ваши блокировки базы данных все еще удерживаются. Сначала рассчитайте, что можете, затем откройте транзакцию для наименьшего набора чтений и записей, которые должны быть атомарными.
2. Стандартизируйте порядок доступа к ресурсам
Это, пожалуй, самая важная стратегия предотвращения. Если каждый фрагмент кода, который взаимодействует с двумя конкретными таблицами (например, orders и inventory), всегда пытается заблокировать таблицы (или строки) в одном и том же порядке (например, orders, затем inventory), циклические зависимости становятся невозможными.
| Транзакция A | Транзакция B |
|---|---|
| Блокировка таблицы X | Блокировка таблицы Y |
| Блокировка таблицы Y | Блокировка таблицы X (РИСК ВЗАИМОБЛОКИРОВКИ) |
Если бы обе транзакции следовали последовательности (X затем Y), транзакция B просто ждала бы завершения A, предотвращая взаимоблокировку.
Это также относится к строкам. Если перевод перемещает деньги между двумя счетами, всегда блокируйте сначала меньший ID счета, а затем больший ID счета:
START TRANSACTION;
SELECT account_id, balance
FROM accounts
WHERE account_id IN (100, 200)
ORDER BY account_id
FOR UPDATE;
-- применить дебет и кредит
COMMIT;
Без согласованного порядка два одновременных перевода в противоположных направлениях могут вызвать взаимоблокировку: один блокирует счет 100, в то время как другой блокирует счет 200, и затем каждый ждет другую строку.
3. Используйте SELECT FOR UPDATE стратегически
При чтении данных, которые будут немедленно изменены позже в той же транзакции, используйте SELECT FOR UPDATE, чтобы немедленно получить исключительную блокировку. Это предотвращает изменение той же строки второй транзакцией между вашим чтением и записью. Это не волшебным образом устраняет взаимоблокировки; это делает блокировку явной, чтобы вы могли контролировать порядок и время.
-- Немедленно получить блокировку на указанную строку(и)
SELECT amount FROM accounts WHERE user_id = 123 FOR UPDATE;
-- Выполнить вычисления в приложении
UPDATE accounts SET amount = new_amount WHERE user_id = 123;
COMMIT;
Стратегия предотвращения 2: Индексация и настройка запросов
Плохая индексация является частой коренной причиной, так как она заставляет InnoDB блокировать больше строк, чем необходимо.
1. Убедитесь, что запросы используют индексы для блокировки
Когда MySQL необходимо найти строки на основе условия WHERE, он блокирует записи индекса, соответствующие условию. Если подходящего индекса нет, InnoDB может выполнить полное сканирование таблицы и заблокировать всю таблицу (или большие диапазоны), даже если нужно всего несколько строк.
- Убедитесь, что столбцы, используемые для поиска строк для обновлений, удалений, соединений и сканирования диапазонов, имеют соответствующие индексы.
- Проверьте, что внешние ключи проиндексированы.
Используйте EXPLAIN для операторов, показанных в отчете о взаимоблокировке:
EXPLAIN UPDATE orders
SET status = 'paid'
WHERE user_id = 42 AND status = 'pending';
Если MySQL сканирует большую часть таблицы для обновления нескольких строк, он может удерживать блокировки, которых вы не ожидали. Составной индекс, такой как (user_id, status), может сузить сканирование. Правильный индекс зависит от ваших фактических шаблонов запросов и распределения данных, поэтому подтвердите с помощью EXPLAIN и данных, похожих на производственные.
2. Минимизируйте блокировки промежутков
InnoDB использует блокировки промежутков (блокировки диапазонов между записями индекса) на уровне изоляции по умолчанию REPEATABLE READ для предотвращения фантомных чтений. Хотя они необходимы для согласованности, эти блокировки часто ответственны за взаимоблокировки, когда диапазоны перекрываются.
Если вы имеете дело с операциями записи с высокой степенью параллелизма и можете мириться с другим поведением чтения, рассмотрите возможность переключения уровня изоляции для определенных сеансов на READ COMMITTED.
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
Предупреждение: Изменение уровня изоляции глобально или неосторожно может привести к поведению, которого ваше приложение не ожидало, например, к неповторяющимся чтениям. Используйте
READ COMMITTEDтолько там, где риски поняты и протестированы.
Стратегия разрешения: Логика повторных попыток на стороне приложения
Даже при самых лучших стратегиях предотвращения взаимоблокировки могут иногда возникать при экстремальной нагрузке. Поскольку InnoDB автоматически откатывает жертву, приложение должно быть спроектировано для корректной обработки этой ошибки.
MySQL сообщает о взаимоблокировке с помощью кода ошибки SQL 1213 (ER_LOCK_DEADLOCK).
Реализация повторных попыток транзакции
Приложения должны перехватывать ошибку 1213 и повторять всю транзакцию, если операцию безопасно повторить. Повторная попытка должна начинаться с START TRANSACTION; не запускайте повторно только неудавшийся оператор.
- Перехват ошибки 1213: Соединитель базы данных должен распознавать ошибку взаимоблокировки.
- Ожидание: Введите короткое, случайное время ожидания перед повторной попыткой, чтобы дать блокирующей транзакции время на фиксацию.
- Повтор: Попробуйте выполнить полную последовательность транзакции снова.
- Ограничение повторных попыток: Реализуйте максимальное количество повторных попыток (например, от 3 до 5), прежде чем завершить запрос пользователя с ошибкой, чтобы предотвратить бесконечные циклы.
MAX_RETRIES = 5
for attempt in range(MAX_RETRIES):
try:
db_connection.execute("START TRANSACTION")
# ... сложные операции с базой данных ...
db_connection.execute("COMMIT")
break # Успех
except DeadlockError:
if attempt < MAX_RETRIES - 1:
time.sleep(0.1 * (attempt + 1)) # Экспоненциальная задержка
continue
else:
raise DatabaseFailure("Транзакция не удалась из-за постоянной взаимоблокировки.")
Будьте осторожны с транзакциями, которые вызывают внешние эффекты. Если транзакция отправляет электронное письмо, списывает средства с карты, публикует сообщение или вызывает другой сервис, слепая повторная попытка может продублировать этот побочный эффект. В таких случаях используйте ключи идемпотентности или переместите побочный эффект после фиксации транзакции базы данных.
Расширенные настройки и лучшие практики
Настройка тайм-аута ожидания блокировки
В MySQL есть настройка, определяющая, как долго транзакция должна ждать блокировку, прежде чем отказаться:
SET GLOBAL innodb_lock_wait_timeout = 50; -- Ожидать до 50 секунд
Установка innodb_lock_wait_timeout слишком низким может привести к сбою транзакций при нормальной конкуренции. Установка слишком высоким может заставить запросы пользователей ждать дольше, чем ваше приложение может выдержать. Эта настройка обрабатывает ожидания блокировки, а не само обнаружение взаимоблокировок, поэтому не рассматривайте ее как основное исправление взаимоблокировок.
Сводка лучших практик
| Область | Лучшая практика |
|---|---|
| Проектирование транзакций | Делайте транзакции короткими, выполняйте быстро и немедленно фиксируйте или откатывайте. |
| Порядок блокировки | Установите строгий, стандартизированный порядок доступа и блокировки строк/таблиц во всем приложении. |
| Индексация | Убедитесь, что все столбцы, используемые для поиска или обновления, правильно проиндексированы для эффективного использования блокировки на уровне строк. |
| Диагностика | Регулярно просматривайте вывод SHOW ENGINE INNODB STATUS и журналы ошибок MySQL на предмет повторяющихся шаблонов взаимоблокировок. |
| Обработка приложением | Реализуйте надежную логику повторных попыток на уровне приложения для корректной обработки ошибки SQL 1213. |
Практический поток отладки
Когда срабатывает оповещение о взаимоблокировке в производственной среде, полезным является следующий поток:
- Захватите отчет о взаимоблокировке до того, как он будет перезаписан.
- Определите два оператора и задействованные индексы.
- Восстановите транзакцию вокруг каждого оператора, а не только один запрос.
- Проверьте, блокируют ли пути кода строки или таблицы в разном порядке.
- Выполните
EXPLAINи добавьте или скорректируйте индексы, если сканирование шире, чем ожидалось. - Убедитесь, что логика повторных попыток существует для безопасных, идемпотентных транзакций.
Взаимоблокировки являются частью параллельных транзакционных систем, но повторяющиеся взаимоблокировки обычно исправимы. Более короткие транзакции, согласованный порядок блокировки, лучшие индексы и тщательная логика повторных попыток приносят больше пользы, чем случайная настройка переменных сервера.