При массовой обработке документов в 1С программисты часто прибегают к использованию явных транзакций (НачатьТранзакцию() ... ЗафиксироватьТранзакцию()), ожидая существенного прироста скорости. Однако на практике возникают ситуации, когда замеры показывают идентичные результаты или, что еще хуже, система начинает работать нестабильно. Разберем подробно, почему так происходит, какие подводные камни скрывает в себе механизм транзакций и как правильно организовать массовое проведение документов.
Для начала проанализируем, как 1С работает с базой данных на низком уровне. Каждый раз, когда мы вызываем метод Записать(РежимЗаписиДокумента.Проведение), платформа автоматически открывает транзакцию, если она не была открыта ранее. Это называется неявной транзакцией. После завершения всех процедур (модуль объекта, модули наборов записей регистров) платформа сама выполняет команду Commit (фиксацию).
Если мы обрабатываем 1000 документов в цикле без явной транзакции, СУБД выполнит 1000 операций открытия и 1000 операций фиксации транзакций. Основное время при этом тратится не на сами расчеты, а на физическую запись лога транзакций СУБД на диск. Когда мы обрамляем цикл в НачатьТранзакцию(), мы заставляем систему выполнить одну большую фиксацию в самом конце. Теоретически это должно быть в десятки раз быстрее. Но почему же автор вопроса не увидел разницы?
Одной из причин отсутствия разницы в замерах может быть высокая производительность дисковой подсистемы. Если база данных расположена на современных NVMe SSD, задержки при фиксации мелких транзакций становятся настолько незначительными, что накладные расходы самой платформы 1С на подготовку данных начинают доминировать над временем записи в БД.
Также стоит учитывать настройки SQL-сервера. Если настроена отложенная фиксация транзакций (Lazy Commit), СУБД не дожидается физического сброса данных на диск при каждой команде Commit, что нивелирует выигрыш от использования одной большой транзакции. В таких условиях время выполнения будет практически одинаковым.
Рассмотрим ситуацию с типом базы данных. В файловом варианте (.1CD) использование явной транзакции критически важно для скорости. Файловая СУБД при каждой фиксации блокирует весь файл, что создает огромные задержки. Если автор проводил тесты в клиент-серверном режиме на мощном сервере, разница будет менее заметной, чем в «тяжелой» файловой базе.
Проанализируем пример кода, который часто приводит к ошибкам. Использование визуальных индикаторов внутри транзакции значительно искажает замеры производительности:
// Влияние интерфейса на скорость
Пока ВыборкаДетали.Следующий() Цикл
элементыФормы.Индикатор1.Значение = тек;
тек = тек + 1;
Состояние("проведение... " + сокрЛП(ВыборкаДетали.Регистратор));
ВыбДок = ВыборкаДетали.Регистратор.ПолучитьОбъект();
ВыбДок.Записать(РежимЗаписиДокумента.Проведение);
КонецЦикла;
В данном примере команды Состояние() и обновление Индикатор1 в толстом клиенте могут занимать больше времени, чем сама запись документа. Постоянная перерисовка интерфейса — это «узкое место». Для получения реальных цифр необходимо проводить замеры в коде, который выполняется полностью на сервере без обращения к элементам формы.
Рассмотрим внимательнее конструкцию Попытка ... Исключение, которую применил автор. Это одна из самых опасных ловушек при работе с транзакциями в 1С. Если внутри блока Попытка при вызове Записать() произойдет любая ошибка (например, нехватка остатков или нарушение уникальности), транзакция на уровне СУБД помечается как «битая» (doomed).
Даже если мы перехватили ошибку в блоке Исключение, транзакция остается активной, но в нее больше нельзя ничего записывать. Любая последующая попытка записи следующего документа в этом же цикле приведет к ошибке: «В данной транзакции уже происходили ошибки». В итоге, если упадет первый документ из тысячи, остальные 999 также не будут проведены. Более того, в конце цикла команда ЗафиксироватьТранзакцию() вызовет исключение, и все изменения будут откатаны.
Правильная логика работы с исключениями:
НачатьТранзакцию();
Попытка
// ... логика проведения документов ...
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
Сообщить("Произошла критическая ошибка, транзакция отменена: " + ОписаниеОшибки());
КонецПопытки;
Выясним, почему проведение 20 000 документов в одной транзакции — это плохая идея. Слишком длинная транзакция приводит к следующим проблемам:
Опытным путем установлено, что оптимальный размер порции составляет от 100 до 1000 документов. Разберем, как реализовать порционное проведение:
Счетчик = 0;
РазмерПорции = 500;
НачатьТранзакцию();
Пока Выборка.Следующий() Цикл
Попытка
ДокОбъект = Выборка.Ссылка.ПолучитьОбъект();
ДокОбъект.Записать(РежимЗаписиДокумента.Проведение);
Счетчик = Счетчик + 1;
Исключение
// Здесь можно либо прервать процесс, либо логировать ошибку
// Но помните: после ошибки транзакцию НУЖНО отменить и начать заново
КонецПопытки;
Если Счетчик % РазмерПорции = 0 Тогда
ЗафиксироватьТранзакцию();
НачатьТранзакцию();
КонецЕсли;
КонецЦикла;
Если ТранзакцияАктивна() Тогда
ЗафиксироватьТранзакцию();
КонецЕсли;
Проанализировав ситуацию, мы видим, что отсутствие разницы в скорости может быть вызвано использованием мощного оборудования, неправильным методом замера (влияние интерфейса) или тем, что замеры проводились в разных условиях (например, на разных копиях базы).
Основные выводы для оптимизации:
Попытка ... Исключение внутри транзакции без последующей отмены транзакции в случае ошибки.ТекущаяУниверсальнаяДатаВМиллисекундах(), исключив любые обновления экранных форм.