Как платформа 1С:Предприятие 8.3 выполняет транзакции и ставит блокировки
Содержание
Транзакция в 1С — это группа изменений базы, которая либо записывается целиком, либо целиком отменяется. Её открывает НачатьТранзакцию(), завершает ЗафиксироватьТранзакцию() или ОтменитьТранзакцию(), а каждая запись объекта (Записать(), проведение документа) и так идёт в транзакции, даже если вы её явно не открывали. Блокировки, поставленные внутри, держатся до фиксации или отката.
Типичная ситуация: два кладовщика одновременно проводят расходные накладные по одному товару. Без правильных блокировок оба увидят остаток 10 штук, оба спишут по 8, и в регистре окажется минус. Механизм транзакций и блокировок платформы как раз и не даёт такому случиться — если разработчик им правильно пользуется.
Как устроена транзакция в 1С: явная и неявная
Платформа различает два способа открыть транзакцию.
Неявная. Её открывает сама платформа при записи объекта: справочника, документа, набора записей регистра. Внутри неё выполняются обработчики ПередЗаписью, ПриЗаписи, ОбработкаПроведения. Если в любом из них поставить Отказ = Истина или выдать исключение, вся запись откатывается — вместе с движениями.
Явная. Её открывает разработчик, когда нужно связать несколько записей в одно целое:
НачатьТранзакцию();
Попытка
ДокументПоступление.Записать(РежимЗаписиДокумента.Проведение);
ДокументОплата.Записать(РежимЗаписиДокумента.Проведение);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Если второй документ не провёлся, первый тоже не останется проведённым.
Что важно знать о режиме работы
- 🖥️ Транзакции работают только на сервере. В клиентском модуле
НачатьТранзакцию()недоступна — код выносят в серверную процедуру или общий модуль. - 🪆 Настоящих вложенных транзакций нет. Повторный вызов
НачатьТранзакцию()внутри активной транзакции не создаёт новую, а только увеличивает счётчик. Фиксация произойдёт, когда сработает последний, внешнийЗафиксироватьТранзакцию(). - 🔙 Откат во «вложенной» транзакции отменяет всю внешнюю. Частично откатить кусок работы, как точкой сохранения в SQL, средствами встроенного языка нельзя.
- 🔎 Проверить, открыта ли транзакция, можно функцией
ТранзакцияАктивна().
Из второго пункта следует простое правило: вызывайте запись объекта внутри своей транзакции спокойно — неявная транзакция платформы «вольётся» в вашу и не зафиксирует данные раньше времени.
Механизм блокировок: автоматический и управляемый режим
Режим задаётся в свойствах корня конфигурации, свойство «Режим управления блокировкой данных». Вариантов три: «Автоматический», «Управляемый» и «Автоматический и управляемый». Последний позволяет задать режим для каждого объекта метаданных отдельно — его используют при переводе старых конфигураций.
| Режим | Кто ставит блокировки | Где работает |
|---|---|---|
| Автоматический | СУБД сама, по своим правилам | Старые конфигурации, написанные под 8.0–8.1 |
| Управляемый | Менеджер блокировок платформы по команде из кода | Все современные типовые: БП 3.0, ЗУП 3.1, УТ 11, ERP |
| Автоматический и управляемый | Зависит от настройки объекта | Переходный вариант |
В автоматическом режиме СУБД держит блокировки грубо: при высоком уровне изоляции читающая транзакция мешает пишущей, и пользователи часто ждут друг друга без реальной нужды. Управляемый режим ставит блокировку ровно на те данные, которые указал разработчик.
Управляемая блокировка в коде
Типичный пример — заблокировать остатки по товарам и складу перед контролем:
Блокировка = Новый БлокировкаДанных;
Элемент = Блокировка.Добавить("РегистрНакопления.ТоварыНаСкладах");
Элемент.Режим = РежимБлокировкиДанных.Исключительный;
Элемент.УстановитьЗначение("Склад", Склад);
Элемент.ИсточникДанных = ТаблицаТоваров;
Элемент.ИспользоватьИзИсточникаДанных("Номенклатура", "Номенклатура");
Блокировка.Заблокировать();
Здесь блокируются только пары «склад + товар» из документа. Второй пользователь, который проводит накладную по другим товарам, ждать не будет.
Два режима элемента блокировки:
- 🔒 Исключительный — никто другой не может поставить на те же данные ни исключительную, ни разделяемую блокировку. Нужен, когда вы читаете данные, чтобы потом их изменить.
- 🤝 Разделяемый — несколько транзакций могут держать его одновременно, но исключительную блокировку при этом никто не получит. Нужен, когда важно, чтобы данные не поменялись, пока вы их читаете.
Блокировка имеет смысл только внутри транзакции и снимается автоматически при её фиксации или откате. Снять её раньше нельзя. Запись в регистр сама по себе тоже ставит исключительную управляемую блокировку на записываемые данные.
Конструкция запроса ДЛЯ ИЗМЕНЕНИЯ работает только в автоматическом режиме. В управляемом платформа её игнорирует — нужна явная БлокировкаДанных.
Объектные блокировки — это другое
Их часто путают с транзакционными. Объектная блокировка — это метод Заблокировать() у объекта или ЗаблокироватьДанныеДляРедактирования(). Она не даёт двум пользователям одновременно открыть на редактирование одну карточку и живёт, пока открыта форма или сеанс, а не транзакция.
Есть и оптимистическая проверка: при записи платформа сравнивает версию данных объекта. Если пока вы держали форму открытой, объект успел записать кто-то другой, запись будет отклонена с сообщением о том, что данные изменены другим пользователем.
Уровни изоляции транзакций на разных СУБД
Уровень изоляции определяет, что транзакция видит из чужих незафиксированных изменений. Его платформа выбирает сама, вручную в коде он не задаётся.
| Вариант базы | Как изолированы транзакции |
|---|---|
| Файловая | Блокируются таблицы целиком. Пока одна транзакция пишет в регистр, другие ждут доступа ко всей таблице |
| Клиент-сервер, автоматический режим | Высокие уровни изоляции СУБД (Repeatable Read, Serializable): блокировки на чтение держатся до конца транзакции |
| Клиент-сервер, управляемый режим, MS SQL | Read Committed; при режиме совместимости 8.3 и новее — с версионированием строк (Read Committed Snapshot), читатели не ждут писателей |
| Клиент-сервер, управляемый режим, PostgreSQL | Read Committed за счёт версионности самой СУБД; согласованность обеспечивают управляемые блокировки платформы |
Что это значит на практике:
- 📄 Запрос внутри транзакции в управляемом режиме видит последние зафиксированные данные, но они могут измениться к следующему запросу. Поэтому ставьте блокировку до чтения остатков, а не после.
- 🗂️ В файловой базе нет смысла оптимизировать блокировки по измерениям — всё равно блокируется таблица. При 5–10 активных пользователях, которые проводят документы, это главная причина ожиданий и повод переходить на клиент-сервер.
- 🧾 Отчёты и списки вне транзакции читают зафиксированные данные и проведению не мешают.
Фиксация, откат и обработка ошибок в транзакции
Главная ловушка здесь — перехват исключения внутри транзакции. Как только внутри неё произошла ошибка, транзакция помечается как испорченная. Её больше нельзя зафиксировать, даже если вы поймали исключение в Попытка и продолжили работу. При попытке фиксации платформа выдаст: В данной транзакции уже происходили ошибки!.
Неправильно:
НачатьТранзакцию();
Для Каждого Строка Из ТаблицаДокументов Цикл
Попытка
Строка.Объект.Записать(РежимЗаписиДокумента.Проведение);
Исключение
// «пропустим документ, проведём остальные»
КонецПопытки;
КонецЦикла;
ЗафиксироватьТранзакцию(); // упадёт, если хоть один документ не провёлся
Если нужно провести всё, что проводится, и пропустить проблемные документы, каждому документу нужна своя транзакция, а цикл — снаружи.
Правила хорошего тона
- Вызывайте
НачатьТранзакцию()передПопытка, а не внутри неё. Иначе при ошибке в самом начале блокИсключениепопробует отменить транзакцию, которая не начиналась. - В блоке
Исключениевсегда вызывайтеОтменитьТранзакцию()и затемВызватьИсключение— чтобы ошибку увидел вызывающий код и внешняя транзакция тоже откатилась. - Количество
НачатьТранзакцию()и пар «фиксация/отмена» должно совпадать на любом пути выполнения, включаяВозвратиз середины процедуры. - Запись в журнал регистрации внутри транзакции по умолчанию откатится вместе с ней. Чтобы сообщение об ошибке сохранилось, используйте у
ЗаписьЖурналаРегистрации()режим транзакцииРежимТранзакцииЗаписиЖурналаРегистрации.Независимая.
Отказ = Истина в обработчике записи — тоже откат. Если ваш код вызывает Записать() внутри своей транзакции, будьте готовы к исключению и обрабатывайте его по тем же правилам.
Попытка либо дайте каждой операции отдельную транзакцию.БлокировкаДанных на регистр по измерениям документа перед запросом остатков.Взаимоблокировки (deadlock): почему возникают и как их убрать
Взаимоблокировка — это когда транзакция А ждёт данные, занятые транзакцией Б, а Б ждёт данные, занятые А. Сами они не разойдутся никогда. Менеджер блокировок платформы (или СУБД в автоматическом режиме) обнаруживает такой цикл и прерывает одну из транзакций с ошибкой, вторая продолжает работу.
Три самых частых сценария:
- 🔁 Повышение блокировки. Обе транзакции читают остатки по товару под разделяемой блокировкой, потом обе хотят записать движения. Каждая ждёт, пока другая отпустит разделяемую блокировку. Лечится исключительной блокировкой сразу.
- 🔀 Разный порядок. Документ А блокирует сначала склад 1, потом склад 2, а документ Б — наоборот. Лечится единым порядком: например, сортировкой таблицы товаров перед блокировкой и записью.
- 🧩 Смешанный режим. Часть объектов в автоматическом режиме, часть — в управляемом. Блокировки СУБД и платформы пересекаются непредсказуемо. Лечится доводкой перевода на управляемый режим.
Как найти виновника
Включите технологический журнал на сервере 1С через файл logcfg.xml. Для управляемых блокировок полезны события:
| Событие | Что показывает |
|---|---|
TLOCK | Каждая управляемая блокировка: пространство, режим, значения полей |
TTIMEOUT | Таймаут ожидания: кто ждал и кто держал |
TDEADLOCK | Взаимоблокировка: участники и ресурсы |
По TDEADLOCK видно, какие две операции столкнулись и на каких данных. Дальше смотрите код их проведения и выравнивайте порядок и режим блокировок.
Событие TLOCK очень объёмное. На нагруженной базе журнал растёт на гигабайты за час. Включайте его на короткое время, с фильтром по нужной базе и с ограничением хранения в logcfg.xml.
Ожидания и производительность: как ускорить транзакции
Чем дольше транзакция держит блокировки, тем дольше ждут остальные. Отсюда большая часть приёмов.
Что делать коротким
- ⏱️ Не держите транзакцию открытой во время диалога. Вопрос пользователю, ожидание ответа веб-сервиса, загрузка файла — всё это выносится до
НачатьТранзакцию()или после фиксации. - 📦 Обрабатывайте большие объёмы порциями. Загрузка 50 000 документов одной транзакцией заблокирует регистры на всё время загрузки. Разбейте на порции по 100–500 документов с фиксацией после каждой.
- 🌐 Не вызывайте внешние сервисы внутри транзакции. HTTP-запрос, который висит 30 секунд, держит блокировки те же 30 секунд, а при откате внешнее действие всё равно не отменится.
Что делать точным
- 🎯 Блокируйте по измерениям, а не весь регистр. Блокировка без
УстановитьЗначениеили источника данных ставится на весь регистр, и проведение превращается в очередь. - 🧮 Контроль остатков — после записи движений. Сначала записать движения, потом одним запросом проверить, не ушёл ли остаток в минус, и при ошибке отменить запись. Так блокировку ставит сама запись, а лишнее чтение до неё не нужно. Так проводят документы современные типовые конфигурации.
- 🗃️ Используйте разделители итогов. Свойство регистра накопления «Разрешить разделение итогов» позволяет параллельным транзакциям писать итоги без ожидания друг друга. В типовых оно включено для основных регистров.
Что проверить на сервере
Таймаут ожидания управляемой блокировки по умолчанию — 20 секунд. Увеличивать его — не решение: пользователи будут дольше висеть, а конфликт останется. Ищите, какая транзакция держит блокировку так долго.
Для поиска долгих транзакций вне журнала подходит консоль кластера: в списке сеансов видно, какой сеанс сейчас выполняет вызов и сколько времени ушло на ожидание СУБД и блокировок.
Завершение «зависшего» сеанса в консоли кластера откатит его незафиксированную транзакцию, а пользователь потеряет несохранённую работу. Предупредите его заранее или дождитесь, пока он сам закончит операцию.
Частые вопросы о транзакциях в 1С
Можно ли откатить только часть транзакции?
Видят ли другие пользователи незафиксированные изменения?
ЗафиксироватьТранзакцию().