Перейти к содержимому
Tez Base

Дедлок 1205 при проведении, а движения не пересекаются: что делать

· Опубликовано:

Блокировки и дедлоки

Если вы попали сюда с ошибкой 1205 deadlock victim при проведении и уже проверили, что документы пишут в разные ключи, дальше можно не искать: на таблице движений конфликта нет и не будет. Он на другой таблице того же регистра.

Ниже не разбор одного инцидента, а порядок действий. Сначала определитель, ваш это класс отказов или соседний, потому что три похожих симптома лечатся по-разному и путать их дорого. Потом три запроса, которыми это проверяется на своей базе. Потом разбор вариантов лечения с ценой каждого и отдельно список того, что в этом случае не помогает, хотя выглядит логично.

Сразу про границы, чтобы не было обманутых ожиданий: частотных цифр в этом материале нет (ни дедлоков в сутки, ни доли пострадавших проводок), и ни один из вариантов лечения мы не замеряли на замере до и после. Механизм доказан на живой системе, выбор фикса под ваши цифры остаётся за вами.

Определитель: три класса, которые дают один и тот же 1205

Практически весь разбор упирается в один вопрос: что стоит в ресурсе графа взаимоблокировки. Ответ на него сразу отсекает две трети работы.

Что в ресурсе графаКакой это классЧто помогает
Страницы и объекты таблицы движений (pagelock, objectlock на _AccumRg<N>)Запрос сканирует лишнее и держит замки дольше нужногоИндекс, переписывание запроса
Ключ индекса таблицы итогов (keylock на _AccumRgT<N>), режимы замков составныеКонкуренция за агрегат, разбирается здесьСужение диапазона чтения, вынос чтения из транзакции
Один объект, замок повышается с разделяемого на исключительныйКонверсия замка внутри одной операцииПорядок захвата, канон обращения к объектам

Средняя строка и есть предмет этого материала. Верхняя разобрана у нас отдельно, в статье про поиск причины взаимоблокировок по частотам: там дедлоки снял индекс, потому что запрос читал таблицу целиком. Если в вашем графе страницы и объекты, идите туда, здесь написанное к вам не относится.

Разница между классами не косметическая. Индекс, который вылечил первый класс, на втором не сдвинет ничего, и наоборот. Поэтому определитель стоит первым, а механизм ниже.

Шаг 1: снять граф и посмотреть, что в ресурсе

Графы взаимоблокировок лежат в кольцевом буфере сессии system_health. Запрос вытаскивает их и сразу показывает, какие объекты фигурируют в ресурсах, чтобы не читать XML глазами:

-- графы взаимоблокировок из system_health с именами объектов в ресурсах
WITH src AS (
    SELECT CAST(st.target_data AS XML) AS td
    FROM sys.dm_xe_session_targets AS st
    JOIN sys.dm_xe_sessions        AS s ON s.address = st.event_session_address
    WHERE s.name = 'system_health' AND st.target_name = 'ring_buffer'
),
grafy AS (
    SELECT n.x.value('@timestamp', 'datetime2')            AS kogda,
           n.x.query('.//deadlock')                        AS graf
    FROM src
    CROSS APPLY src.td.nodes(
        'RingBufferTarget/event[@name="xml_deadlock_report"]') AS n(x)
)
SELECT g.kogda,
       r.x.value('@objectname', 'nvarchar(256)') AS obyekt,
       r.x.value('local-name(.)', 'nvarchar(64)') AS tip_resursa,
       r.x.value('@mode', 'nvarchar(32)')        AS rezhim_vladeltsa,
       g.graf
FROM grafy AS g
CROSS APPLY g.graf.nodes('deadlock/resource-list/*') AS r(x)
ORDER BY g.kogda DESC;

Имя с _AccumRgT в колонке объекта означает попадание в этот класс. Имя _AccumRg без буквы T означает соседний класс из верхней строки определителя.

Две приметы, которые подтверждают попадание окончательно:

  • тип ресурса keylock, то есть ключ индекса. Ни строк, ни страниц в ресурсе этого класса не бывает, потому что диапазонный замок живёт именно на ключе;
  • составные режимы замков: режим из двух частей, где первая описывает закрытый промежуток между ключами, а вторая сам ключ. Когда одна сторона держит режим с исключительной частью, а вторая просит режим с разделяемой, и обе на одном индексе, картина сошлась.

⚠️ Кольцевой буфер маленький и под нагрузкой перетирается за минуты, а ещё сбрасывается перезапуском экземпляра и переключением основного узла. Пустой ответ не означает, что дедлоков не было. Как отличить три разных вида пустоты в буфере, разобрано отдельно; если инцидент повторяющийся, поднимите свою сессию расширенных событий в файл, она переживает и шторм, и перезапуск службы.

Шаг 2: перевести имя таблицы в имя регистра

Платформа раскладывает регистр накопления на несколько физических таблиц, и номер <N> у них сквозной: движения и итоги одного регистра носят один номер.

Имя таблицыЧто это
_AccumRg<N>движения, строки с документом-регистратором
_AccumRgT<N>итоги остатков, тот самый агрегат
_AccumRgTn<N>итоги оборотов
_AccumRgOpt<N>служебная таблица настроек итогов
_AccumRgChng<N>, _AccumRgDl<N>регистрация изменений и удалений

Обратный перевод в имя объекта конфигурации умеет делать сама платформа, справочника соответствий держать не надо:

// Таблица соответствия: физическое имя в СУБД -> объект метаданных.
// Выполняется на сервере, права administrator не нужны.
Структура = ПолучитьСтруктуруХраненияБазыДанных(, Истина);
Для Каждого Строка Из Структура Цикл
    Если СтрНачинаетсяС(Строка.ИмяТаблицыХранения, "_AccumRgT") Тогда
        Сообщить(Строка.ИмяТаблицыХранения + " = "
            + Строка.ИмяТаблицы + " (" + Строка.Назначение + ")");
    КонецЕсли;
КонецЦикла;

Если граф приходится разбирать регулярно, перевод имён стоит автоматизировать сразу: у нас для этого есть трансформатор SQL в запрос 1С, он подставляет имена объектов конфигурации в текст, снятый со стороны СУБД.

Шаг 3: найти в модуле проведения чтение после записи

Последняя проверка делается уже в конфигураторе и занимает минуту. Откройте модуль проведения документа и найдите в нём чтение итогов того же регистра, в который документ пишет движения. Порядок внутри транзакции такой: сначала Движения.Записать(), потом запрос остатка.

Нашли такое сочетание, значит механизм подтверждён и диагностику можно закрывать. Раз уж модуль открыт, стоит проверить и соседний класс дефектов: ставятся ли управляемые замки в нём по-настоящему - блокировка умеет молча не ставиться, и код при этом выглядит правильным.

Здесь есть неочевидная деталь, из-за которой это сочетание пропускают глазами. Обновление итогов не пишется в коде отдельной строкой. Платформа обновляет их внутри записи набора записей, в той же транзакции, автоматически. Разработчик видит одну строку записи движений и не видит второго обращения, которое ушло на агрегат. Искать надо не два обращения к итогам, а запись движений плюс любой запрос остатка ниже по тексту.

Почему конфликт оказывается на итогах: короткая справка

Механизм нужен для выбора лечения, поэтому изложу его сжато, без разбора инцидента.

У регистра с включёнными итогами есть вторая таблица, где лежат посчитанные остатки в разрезе измерений, без привязки к документу. Отсюда всё и растёт: движения индивидуальны, итоги общие. Два документа по одному складу и одной номенклатуре пишут разные строки движений и обновляют одну строку итогов.

Дальше две параллельные проводки делают одно и то же. Каждая пишет свои движения и получает на итогах исключительные диапазонные замки на своих ключах, пока конфликта нет. Каждая читает остаток и просит разделяемый замок на диапазоне, который включает ключи соседа. Дальше взаимное ожидание и жертва с кодом 1205.

Два следствия, которые определяют выбор лечения.

Диапазон чтения шире, чем кажется. Диапазонный замок закрывает не только найденный ключ, но и промежуток до соседнего: так сервер гарантирует, что в этот промежуток никто не вставит строку до конца транзакции. Ширина захвата определяется тем, сколько ключей пришлось просмотреть по дороге, а не тем, сколько строк запрос вернул. Поэтому запрос остатка по всему складу закрывает диапазон всех номенклатур склада, даже если реально нужна одна позиция.

Сталкиваются две копии одной нормальной операции. Не проведение против закрытия месяца и не обмен против отчёта, а два экземпляра обычной проводки. Отсюда неприятное свойство: чем ровнее идёт рабочий день, тем чаще воспроизводится отказ. Нет сценария, который можно перестать запускать, есть проведение документов. Отсюда же тупик в разговоре с пользователями: вопрос “что вы запускали в этот момент” получает честный ответ “ничего особенного”, и разбор глохнет. Ответ правильный, вопрос неправильный.

Заодно это объясняет ощущение внезапности. Число пар, которые могут встретиться, при N одновременных проводках равно N(N-1)/2: пять проводок дают 10 пар, десять дают 45, двадцать дают 190. Это выведено из формулы, а не замерено. Но вывод для планирования из неё прямой: оценивать риск по средней нагрузке нельзя, считать надо по пиковой параллельности, потому что вклад в частоту даёт именно она.

Посмотреть, по какому индексу вообще ходят запросы к таблице итогов, полезно перед выбором лечения: ширина захвата определяется им.

-- индексы таблицы итогов и как их используют: seek против scan
SELECT i.name                       AS indeks,
       i.type_desc                  AS tip,
       s.user_seeks                 AS poiskov,
       s.user_scans                 AS skanirovaniy,
       s.user_updates               AS obnovleniy,
       s.last_user_scan             AS posledniy_scan
FROM sys.indexes AS i
LEFT JOIN sys.dm_db_index_usage_stats AS s
       ON s.object_id = i.object_id
      AND s.index_id  = i.index_id
      AND s.database_id = DB_ID()
WHERE i.object_id = OBJECT_ID('_AccumRgT15719')   -- имя вашей таблицы итогов
ORDER BY s.user_scans DESC;

Сканирование по таблице итогов читается однозначно: запрос просматривает ключей больше, чем ему нужно, и закрывает диапазон соответственно.

Что помогает, и чего это стоит

Ни один из вариантов ниже мы не выкатывали и не замеряли на этой системе. Поэтому не “рекомендуем”, а перечень с ценой, чтобы вы выбирали под свои ограничения.

Сузить диапазон чтения до тех же ключей, которые пишутся. Самый локальный вариант: проверка остатка по конкретному складу и конкретной номенклатуре, без захода на измерение целиком, сжимает диапазон захвата. Цена низкая, прикладную логику не трогает. Гарантий тоже нет: пока диапазоны хоть где-то пересекаются, механизм жив, просто срабатывает реже. Разумное место, чтобы начать.

Вынести чтение итогов за пределы транзакции записи. Прямое устранение конструкции: разведённые по разным транзакциям читатель и писатель не образуют взаимного ожидания. Цена в том, что логика проведения перестаёт видеть собственные движения в момент проверки, и это меняет прикладной смысл. Требует пересборки проверок остатка и годится не везде.

Снизить пиковую параллельность проведения. Из квадратичной зависимости следует, что даже небольшое снижение числа одновременных проводок ощутимо снижает частоту. Самый дешёвый и самый нелюбимый вариант: лечит следствие, никого не радует, зато работает сегодня.

Отключить использование итогов у регистра. Конкурировать станет не на чем, второй таблицы просто не будет. Цена высокая и очевидная: запросы остатков начнут считать по движениям, и чтение просядет тем сильнее, чем больше накоплено. На большом регистре это лечение хуже болезни.

Сменить уровень изоляции на тот, где нет диапазонных замков. Убирает механизм целиком и тянет за собой всю систему: то, что раньше сериализовалось само собой, становится гонкой. Отдельная большая работа с перебором всех мест, где на сериализацию молча полагались. Одним регистром дело не ограничится.

Из пяти вариантов первые три не трогают ни прикладную логику, ни модель корректности. Два последних тянут на проект и галочкой в конфигураторе не закрываются.

Что в этом классе не поможет

Список важнее, чем кажется: на каждый пункт обычно уходит несколько дней работы, прежде чем становится видно, что частота не сдвинулась.

Индекс на таблицу движений и переписывание запроса. Лечит соседний класс из верхней строки определителя. Здесь запрос после оптимизации честно работает быстрее, а дедлоки остаются: замки, из-за которых всё встало, набраны строкой выше.

Канонический порядок захвата объектов. В этой конструкции нет двух объектов, захваченных в разном порядке. Порядок у обеих транзакций одинаковый: сначала запись, потом чтение. Инверсия происходит по типу замка на пересекающихся диапазонах, и правило “обращаемся к объектам всегда в одной последовательности” её не трогает.

Управляемые блокировки и разбор их порядка. Диапазонных блокировок в терминах 1С не существует вовсе: они возникают уровнем ниже, на сервере СУБД, когда транзакция идёт под уровнем изоляции, который берёт диапазонные замки. Анализ на прикладном уровне здесь бесполезен по определению. Кстати, сам факт диапазонных замков в графе уже говорит, в каком режиме работал этот кусок конфигурации, проверять отдельно не нужно.

Разделение итогов. Этот вопрос задают первым, поэтому отвечу подробнее. Режим добавляет в таблицу итогов служебное измерение-разделитель, параллельные сеансы пишут в строки с разными его значениями, конкуренция писателей за одну строку исчезает. Против нашей конструкции по механизму работать не должно: разделитель разносит записи, но не разносит чтение. Запрос остатка обязан пройти все значения разделителя, иначе получит неполную сумму, значит его диапазон по-прежнему накрывает ключи соседа. Разделение лечит столкновение писателей и не трогает столкновение писателя с читателем.

Проверить, включено ли оно у вас, можно запросом, не гадая по галочкам в конфигураторе:

-- у каких регистров разделение итогов реально включено:
-- в таблице итогов появляется служебная колонка _Splitter
SELECT t.name AS tablitsa_itogov,
       MAX(CASE WHEN c.name = '_Splitter' THEN 1 ELSE 0 END) AS razdelenie_vklyucheno
FROM sys.tables  AS t
JOIN sys.columns AS c ON c.object_id = t.object_id
WHERE t.name LIKE '\_AccumRgT%' ESCAPE '\'
GROUP BY t.name
ORDER BY razdelenie_vklyucheno DESC, t.name;

Оговорюсь честно: разделение итогов на этой системе не включали и не замеряли. Написанное выше вывод из механизма, замера за ним нет, и возражению с замером я буду рад больше, чем согласию.

Перед тем как что-то менять

Прежде чем трогать уровень изоляции или выключать итоги, стоит убедиться, что дело вообще в конкуренции за данные, а не в настройках сервера. У нас был случай, когда кассы вставали из-за MAXDOP, а не из-за блокировок, и лечилось это двумя параметрами. Общий чек-ап настроек SQL Server под 1С быстрее делать готовой обработкой, чем собирать запросы руками.

И держите в голове разницу между двумя вопросами. Механизм объясняет, почему отказ возможен, и ничего не говорит о том, почему он начался именно тогда. Конструкция обычно существует в коде годами, а жаловаться начинают в конкретный момент: выросла параллельность, сменился профиль работы, добавился фоновый процесс. Разница между этими двумя вопросами и есть разница между “поняли” и “починили”. Второй вопрос закрывается замером по истории нагрузки, и так мы работаем в оптимизации производительности 1С: сначала час, когда отказ начался, потом правка и замер до и после.

Границы применимости

Механизм описан для регистра накопления с включёнными итогами. Без итогов второй таблицы не существует и конфликтовать не на чем.

Диапазонные замки возникают только под уровнем изоляции, который их использует. Вне этого условия картина отказа будет другой, и переносить разбор нельзя.

Это не конверсия разделяемого замка в исключительный на одном объекте. Там одна операция повышает тип замка, здесь две операции встречаются на пересекающихся диапазонах. Нижняя строка определителя, лечится по-другому.

Частоты и замера до и после здесь нет. Механизм доказан, масштаб не измерен, лечение не выбиралось. Всё, что похоже на оценку масштаба, выведено из формулы для пар и помечено отдельно.

Если вы упёрлись в этот класс отказов и нужен разбор графа на своих данных, напишите нам: диагностика конкуренции за данные это как раз наша работа, начинаем с экспресс-аудита.

Могу починить это за вас

Находим настоящую причину, почему 1С тормозит и как ускорить её работу - по технологическому журналу и планам запросов, а не наугад. Ускоряем проведение документов, отчёты и обмены в разы. Удалённо по всему Казахстану и СНГ.

Стоимость
150 000 - 450 000 ₸ТЗ и оптимизация под ключ, без налогов; доработки сверх плана - единым счётом по факту диагностики
типичный результат по "тяжёлым" операциям
часы → минуты

Рано писать? Измерьте сами

Чек-ап СУБД под 1С: 40+ проверок с вердиктом по каждой

Открыть обработку

INFOSTART TECH EVENT 2026: конференция для 1с-специалистов. если собираетесь, регистрируйтесь по нашей ссылке. Регистрация →

Читайте также

Нейросеть для проверки кода 1С: как проверить её на своём коде до внедрения

Прежде чем отдавать нейросети ревью кода 1С, её стоит принять на своём коде: у нас одна и та же локальная модель нашла 11 дефектов из 13 на учебных процедурах и 0 из 5 на рабочих. Разбираем порядок приёмки: какие два числа считать, из чего собрать проверочный набор, как поднять стенд без интернета и по каким признакам видно, что замер врёт.

Технологический журнал 1С: как собрать данные о тормозах

План короткого сбора технологического журнала 1С: вопрос, события, окно наблюдения и проверка причины. Как отличить ожидание от выполнения.

Управляемая блокировка 1С не работает: шесть причин, по которым замок не ставится

В коде стоит БлокировкаДанных, ревью пройдено, а потерянные обновления и минусовые остатки продолжаются. Разбор таких историй почти всегда упирается в одно: замок, на который рассчитывали, не ставился вовсе - и платформа об этом не сообщала. Чек-лист из шести мест, где блокировка теряется молча: режим блокировок объекта, флаг в процедуре-обёртке, чужая короткая транзакция, разделяемый режим, проверка вне транзакции и включение снимка на чтение. С кодом, который можно сверить со своей конфигурацией за вечер.