Как платформа 1С:Предприятие 8.3 выполняет транзакции и ставит блокировки

· 10 мин

Содержание

Транзакция в 1С — это группа изменений базы, которая либо записывается целиком, либо целиком отменяется. Её открывает НачатьТранзакцию(), завершает ЗафиксироватьТранзакцию() или ОтменитьТранзакцию(), а каждая запись объекта (Записать(), проведение документа) и так идёт в транзакции, даже если вы её явно не открывали. Блокировки, поставленные внутри, держатся до фиксации или отката.

Типичная ситуация: два кладовщика одновременно проводят расходные накладные по одному товару. Без правильных блокировок оба увидят остаток 10 штук, оба спишут по 8, и в регистре окажется минус. Механизм транзакций и блокировок платформы как раз и не даёт такому случиться — если разработчик им правильно пользуется.

Как устроена транзакция в 1С: явная и неявная

Платформа различает два способа открыть транзакцию.

Неявная. Её открывает сама платформа при записи объекта: справочника, документа, набора записей регистра. Внутри неё выполняются обработчики ПередЗаписью, ПриЗаписи, ОбработкаПроведения. Если в любом из них поставить Отказ = Истина или выдать исключение, вся запись откатывается — вместе с движениями.

Явная. Её открывает разработчик, когда нужно связать несколько записей в одно целое:

НачатьТранзакцию();
Попытка
    ДокументПоступление.Записать(РежимЗаписиДокумента.Проведение);
    ДокументОплата.Записать(РежимЗаписиДокумента.Проведение);
    ЗафиксироватьТранзакцию();
Исключение
    ОтменитьТранзакцию();
    ВызватьИсключение;
КонецПопытки;

Если второй документ не провёлся, первый тоже не останется проведённым.

Что важно знать о режиме работы

  • 🖥️ Транзакции работают только на сервере. В клиентском модуле НачатьТранзакцию() недоступна — код выносят в серверную процедуру или общий модуль.
  • 🪆 Настоящих вложенных транзакций нет. Повторный вызов НачатьТранзакцию() внутри активной транзакции не создаёт новую, а только увеличивает счётчик. Фиксация произойдёт, когда сработает последний, внешний ЗафиксироватьТранзакцию().
  • 🔙 Откат во «вложенной» транзакции отменяет всю внешнюю. Частично откатить кусок работы, как точкой сохранения в SQL, средствами встроенного языка нельзя.
  • 🔎 Проверить, открыта ли транзакция, можно функцией ТранзакцияАктивна().

Из второго пункта следует простое правило: вызывайте запись объекта внутри своей транзакции спокойно — неявная транзакция платформы «вольётся» в вашу и не зафиксирует данные раньше времени.

Механизм блокировок: автоматический и управляемый режим

Режим задаётся в свойствах корня конфигурации, свойство «Режим управления блокировкой данных». Вариантов три: «Автоматический», «Управляемый» и «Автоматический и управляемый». Последний позволяет задать режим для каждого объекта метаданных отдельно — его используют при переводе старых конфигураций.

РежимКто ставит блокировкиГде работает
АвтоматическийСУБД сама, по своим правиламСтарые конфигурации, написанные под 8.0–8.1
УправляемыйМенеджер блокировок платформы по команде из кодаВсе современные типовые: БП 3.0, ЗУП 3.1, УТ 11, ERP
Автоматический и управляемыйЗависит от настройки объектаПереходный вариант

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

Управляемая блокировка в коде

Типичный пример — заблокировать остатки по товарам и складу перед контролем:

Блокировка = Новый БлокировкаДанных;
Элемент = Блокировка.Добавить("РегистрНакопления.ТоварыНаСкладах");
Элемент.Режим = РежимБлокировкиДанных.Исключительный;
Элемент.УстановитьЗначение("Склад", Склад);
Элемент.ИсточникДанных = ТаблицаТоваров;
Элемент.ИспользоватьИзИсточникаДанных("Номенклатура", "Номенклатура");
Блокировка.Заблокировать();

Здесь блокируются только пары «склад + товар» из документа. Второй пользователь, который проводит накладную по другим товарам, ждать не будет.

Два режима элемента блокировки:

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

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

Конструкция запроса ДЛЯ ИЗМЕНЕНИЯ работает только в автоматическом режиме. В управляемом платформа её игнорирует — нужна явная БлокировкаДанных.

Объектные блокировки — это другое

Их часто путают с транзакционными. Объектная блокировка — это метод Заблокировать() у объекта или ЗаблокироватьДанныеДляРедактирования(). Она не даёт двум пользователям одновременно открыть на редактирование одну карточку и живёт, пока открыта форма или сеанс, а не транзакция.

Есть и оптимистическая проверка: при записи платформа сравнивает версию данных объекта. Если пока вы держали форму открытой, объект успел записать кто-то другой, запись будет отклонена с сообщением о том, что данные изменены другим пользователем.

Уровни изоляции транзакций на разных СУБД

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

Вариант базыКак изолированы транзакции
ФайловаяБлокируются таблицы целиком. Пока одна транзакция пишет в регистр, другие ждут доступа ко всей таблице
Клиент-сервер, автоматический режимВысокие уровни изоляции СУБД (Repeatable Read, Serializable): блокировки на чтение держатся до конца транзакции
Клиент-сервер, управляемый режим, MS SQLRead Committed; при режиме совместимости 8.3 и новее — с версионированием строк (Read Committed Snapshot), читатели не ждут писателей
Клиент-сервер, управляемый режим, PostgreSQLRead Committed за счёт версионности самой СУБД; согласованность обеспечивают управляемые блокировки платформы

Что это значит на практике:

  • 📄 Запрос внутри транзакции в управляемом режиме видит последние зафиксированные данные, но они могут измениться к следующему запросу. Поэтому ставьте блокировку до чтения остатков, а не после.
  • 🗂️ В файловой базе нет смысла оптимизировать блокировки по измерениям — всё равно блокируется таблица. При 5–10 активных пользователях, которые проводят документы, это главная причина ожиданий и повод переходить на клиент-сервер.
  • 🧾 Отчёты и списки вне транзакции читают зафиксированные данные и проведению не мешают.

Фиксация, откат и обработка ошибок в транзакции

Главная ловушка здесь — перехват исключения внутри транзакции. Как только внутри неё произошла ошибка, транзакция помечается как испорченная. Её больше нельзя зафиксировать, даже если вы поймали исключение в Попытка и продолжили работу. При попытке фиксации платформа выдаст: В данной транзакции уже происходили ошибки!.

Неправильно:

НачатьТранзакцию();
Для Каждого Строка Из ТаблицаДокументов Цикл
    Попытка
        Строка.Объект.Записать(РежимЗаписиДокумента.Проведение);
    Исключение
        // «пропустим документ, проведём остальные»
    КонецПопытки;
КонецЦикла;
ЗафиксироватьТранзакцию(); // упадёт, если хоть один документ не провёлся

Если нужно провести всё, что проводится, и пропустить проблемные документы, каждому документу нужна своя транзакция, а цикл — снаружи.

Правила хорошего тона

  1. Вызывайте НачатьТранзакцию() перед Попытка, а не внутри неё. Иначе при ошибке в самом начале блок Исключение попробует отменить транзакцию, которая не начиналась.
  2. В блоке Исключение всегда вызывайте ОтменитьТранзакцию() и затем ВызватьИсключение — чтобы ошибку увидел вызывающий код и внешняя транзакция тоже откатилась.
  3. Количество НачатьТранзакцию() и пар «фиксация/отмена» должно совпадать на любом пути выполнения, включая Возврат из середины процедуры.
  4. Запись в журнал регистрации внутри транзакции по умолчанию откатится вместе с ней. Чтобы сообщение об ошибке сохранилось, используйте у ЗаписьЖурналаРегистрации() режим транзакции РежимТранзакцииЗаписиЖурналаРегистрации.Независимая.

Отказ = Истина в обработчике записи — тоже откат. Если ваш код вызывает Записать() внутри своей транзакции, будьте готовы к исключению и обрабатывайте его по тем же правилам.

Диагностика: выберите симптом

Взаимоблокировки (deadlock): почему возникают и как их убрать

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

Три самых частых сценария:

  • 🔁 Повышение блокировки. Обе транзакции читают остатки по товару под разделяемой блокировкой, потом обе хотят записать движения. Каждая ждёт, пока другая отпустит разделяемую блокировку. Лечится исключительной блокировкой сразу.
  • 🔀 Разный порядок. Документ А блокирует сначала склад 1, потом склад 2, а документ Б — наоборот. Лечится единым порядком: например, сортировкой таблицы товаров перед блокировкой и записью.
  • 🧩 Смешанный режим. Часть объектов в автоматическом режиме, часть — в управляемом. Блокировки СУБД и платформы пересекаются непредсказуемо. Лечится доводкой перевода на управляемый режим.

Как найти виновника

Включите технологический журнал на сервере 1С через файл logcfg.xml. Для управляемых блокировок полезны события:

СобытиеЧто показывает
TLOCKКаждая управляемая блокировка: пространство, режим, значения полей
TTIMEOUTТаймаут ожидания: кто ждал и кто держал
TDEADLOCKВзаимоблокировка: участники и ресурсы

По TDEADLOCK видно, какие две операции столкнулись и на каких данных. Дальше смотрите код их проведения и выравнивайте порядок и режим блокировок.

Событие TLOCK очень объёмное. На нагруженной базе журнал растёт на гигабайты за час. Включайте его на короткое время, с фильтром по нужной базе и с ограничением хранения в logcfg.xml.

Ожидания и производительность: как ускорить транзакции

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

Что делать коротким

  • ⏱️ Не держите транзакцию открытой во время диалога. Вопрос пользователю, ожидание ответа веб-сервиса, загрузка файла — всё это выносится до НачатьТранзакцию() или после фиксации.
  • 📦 Обрабатывайте большие объёмы порциями. Загрузка 50 000 документов одной транзакцией заблокирует регистры на всё время загрузки. Разбейте на порции по 100–500 документов с фиксацией после каждой.
  • 🌐 Не вызывайте внешние сервисы внутри транзакции. HTTP-запрос, который висит 30 секунд, держит блокировки те же 30 секунд, а при откате внешнее действие всё равно не отменится.

Что делать точным

  • 🎯 Блокируйте по измерениям, а не весь регистр. Блокировка без УстановитьЗначение или источника данных ставится на весь регистр, и проведение превращается в очередь.
  • 🧮 Контроль остатков — после записи движений. Сначала записать движения, потом одним запросом проверить, не ушёл ли остаток в минус, и при ошибке отменить запись. Так блокировку ставит сама запись, а лишнее чтение до неё не нужно. Так проводят документы современные типовые конфигурации.
  • 🗃️ Используйте разделители итогов. Свойство регистра накопления «Разрешить разделение итогов» позволяет параллельным транзакциям писать итоги без ожидания друг друга. В типовых оно включено для основных регистров.

Что проверить на сервере

Таймаут ожидания управляемой блокировки по умолчанию — 20 секунд. Увеличивать его — не решение: пользователи будут дольше висеть, а конфликт останется. Ищите, какая транзакция держит блокировку так долго.

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

Завершение «зависшего» сеанса в консоли кластера откатит его незафиксированную транзакцию, а пользователь потеряет несохранённую работу. Предупредите его заранее или дождитесь, пока он сам закончит операцию.

Проверка серверного кода с транзакцией
Выполнено: 0 / 7

Частые вопросы о транзакциях в 1С

Можно ли откатить только часть транзакции?
Нет. Встроенный язык не поддерживает точки сохранения. Откат во вложенном вызове отменяет всю внешнюю транзакцию.
Видят ли другие пользователи незафиксированные изменения?
В управляемом режиме — нет: они читают последнюю зафиксированную версию данных. Изменения становятся видны после ЗафиксироватьТранзакцию().
Транзакция откатится, если у пользователя пропала связь?
Да. Когда сервер теряет сеанс или сеанс завершают, незафиксированная транзакция отменяется, и база остаётся в состоянии до её начала.
Нужна ли блокировка, если я только записываю движения, ничего не читая?
Обычно нет: запись набора записей сама ставит исключительную блокировку на записываемые данные. Явная блокировка нужна, когда решение принимается по прочитанным данным — остаткам, ценам, номеру.
Почему в файловой базе проведение тормозит сильнее, чем на сервере?
В файловом варианте блокируется вся таблица, а не отдельные записи. Пока проводится один документ, остальные ждут, даже если товары разные.