Взаимоблокировки (deadlock)
Что ответить, когда спрашивают: «Два документа проводятся одновременно и оба зависают с ошибкой блокировки. Что произошло и как это лечить?»
Взаимоблокировка (deadlock) — ситуация, когда два (или больше) процесса захватили по ресурсу и каждый ждёт ресурс, который держит другой. Оба ждут вечно, поэтому система принудительно «убивает» одну из транзакций, откатывая её.
Deadlock — любимая тема мидл-собеседований, потому что она проверяет, понимаете ли вы, что блокировки берутся не мгновенно и в определённом порядке. В отличие от обычной блокировки (где один просто ждёт другого и потом проходит), deadlock — это тупик: ждать бесполезно, никто не уступит.
Как это возникает: классический сценарий
Представим два расходных документа, которые списывают два товара — А и Б — но обрабатывают их в разном порядке.
// Документ 1 (в цикле по своим товарам): сначала А, потом Б
Заблокировать(Товар_А); // захватил А
// ... тем временем Документ 2 захватил Б ...
Заблокировать(Товар_Б); // ждёт Б, который держит Документ 2
// Документ 2 (в цикле по своим товарам): сначала Б, потом А
Заблокировать(Товар_Б); // захватил Б
Заблокировать(Товар_А); // ждёт А, который держит Документ 1
Пошагово тупик складывается так:
- Документ 1 заблокировал товар А.
- Документ 2 заблокировал товар Б.
- Документ 1 хочет Б — а его держит Документ 2. Ждёт.
- Документ 2 хочет А — а его держит Документ 1. Ждёт.
- Оба ждут друг друга. СУБД обнаруживает цикл ожидания и откатывает одну транзакцию с ошибкой вида «Конфликт блокировок» / deadlock.
Обратите внимание: сами по себе оба документа корректны. Проблема рождается только из разного порядка захвата одних и тех же ресурсов при параллельной работе.
Главное лекарство — единый порядок доступа
Если все процессы всегда берут ресурсы в одном и том же порядке, тупик математически невозможен: тот, кто первым захватил «младший» ресурс, гарантированно дойдёт до «старшего» — второй просто подождёт в очереди, но не встанет в тупик.
На практике это значит: перед блокировкой упорядочивайте строки табличной части по тому же ключу (например, по ссылке номенклатуры), чтобы любой документ блокировал товары в одинаковой последовательности.
// Единый порядок: сортируем строки перед проведением,
// чтобы ВСЕ документы блокировали номенклатуру одинаково
ТаблицаТоваров = Товары.Выгрузить();
ТаблицаТоваров.Сортировать("Номенклатура");
Блокировка = Новый БлокировкаДанных;
Элемент = Блокировка.Добавить("РегистрНакопления.ТоварыНаСкладах.Остатки");
Элемент.Режим = РежимБлокировкиДанных.Исключительный;
Для Каждого Строка Из ТаблицаТоваров Цикл
Элемент.УстановитьЗначение("Номенклатура", Строка.Номенклатура);
Элемент.УстановитьЗначение("Склад", Склад);
КонецЦикла;
Блокировка.Заблокировать();
Здесь один объект БлокировкаДанных накапливает все нужные значения и ставит блокировку одним вызовом Заблокировать() — это ещё и надёжнее, чем блокировать построчно в цикле, потому что платформа берёт весь набор согласованно.
Как это работает под капотом
Deadlock обнаруживает не 1С, а СУБД (или менеджер управляемых блокировок платформы). Специальный механизм периодически строит граф ожиданий: кто кого ждёт. Если в графе находится цикл — это тупик. Система выбирает «жертву» (обычно транзакцию, которую дешевле откатить), откатывает её и возбуждает исключение. Вторая транзакция после этого спокойно доводит работу до конца.
Важное следствие для ответа: раз одна транзакция откатывается с ошибкой, прикладной код должен быть готов её повторить. В типовых конфигурациях 1С проведение оборачивают в механизм повторных попыток: поймали исключение блокировки — подождали случайный небольшой интервал — попробовали снова. Но повторы лечат симптом; настоящее лечение — единый порядок, чтобы тупиков не было вовсе.
Ещё сценарии deadlock при проведении
- Разный порядок регистров. Один документ пишет сначала в «Товары», потом в «Взаиморасчёты», другой — наоборот. Держите единый порядок обращения к регистрам во всех проведениях.
- Эскалация блокировок. Точечная блокировка по значению может «разрастись» до блокировки страницы/диапазона, если строк много или нет подходящего индекса — и внезапно пересечься с чужой. Индексы (измерения регистра) снижают риск.
- Долгая транзакция. Чем дольше держится блокировка, тем выше шанс, что кто-то в неё упрётся. Не делайте внутри транзакции ничего лишнего — тем более обращений к пользователю.
Как отвечать на собеседовании
«Deadlock — это когда две транзакции захватили по ресурсу и ждут ресурс друг друга; ожидание бесконечно, поэтому СУБД откатывает одну из них. Возникает из-за разного порядка захвата одних и тех же данных. Главное средство — единый порядок обращения к ресурсам: сортировать строки перед блокировкой и одинаково упорядочивать доступ к регистрам, тогда тупик невозможен. Плюс держать транзакции короткими и уметь повторить откаченную транзакцию». Такой ответ показывает, что вы понимаете и причину, и профилактику, и обработку.
Частые ошибки на собеседовании
- Путать deadlock с обычным ожиданием блокировки. Долгое ожидание — это ещё не тупик; тупик — это взаимное ожидание по кругу. Разница принципиальна.
- «Лечится увеличением таймаута». Нет: больший таймаут только оттягивает откат, цикл всё равно не разрешится. Таймаут спасает от долгого ожидания, но не от тупика.
- Забыть про единый порядок. Кандидат называет deadlock, но не может предложить профилактику — это и есть тот ответ, который отличает знающего от заучившего термин.
- Игнорировать повтор транзакции. Даже при единой дисциплине блокировок редкий deadlock возможен, поэтому проведение должно уметь повториться, а не падать пользователю в лицо.
Итоги-шпаргалка
- Deadlock = взаимное ожидание по кругу; система откатывает одну транзакцию.
- Причина — разный порядок захвата одних и тех же ресурсов.
- Лекарство №1 — единый порядок доступа: сортировать строки, одинаково упорядочивать регистры.
- Транзакции держим короткими; блокируем точечно и по индексам.
- Откаченную транзакцию нужно уметь повторить — но повтор лечит симптом, а не причину.