При интеграции крупных информационных систем разработчики часто сталкиваются с задачей импорта колоссальных массивов информации. Например, когда требуется загрузить более 1 миллиона строк (содержащих до 100 колонок) с возможностью автоматического отката в случае ошибок, обеспечением целостности данных и использованием механизмов блокировок. При этом для ускорения процесса крайне желательно использовать многопоточность.
В рамках платформы 1С:Предприятие 8 стандартная «простая транзакция в коде» не способна решить эту задачу в лоб. Мы не можем открыть одну транзакцию СУБД и распределить запись внутри нее на несколько параллельных фоновых заданий — это технически невозможно из-за архитектуры СУБД и самой платформы. Давайте подробно разберем, почему классический подход здесь не работает, и какие продвинутые архитектурные паттерны помогут нам элегантно решить эту проблему.
Попытка выполнить запись или перезапись 1 000 000 объектов в рамках одной транзакции неизбежно приведет к деградации производительности всей системы и аварийному завершению процесса. Проанализируем ключевые причины этого поведения:
Таким образом, мы приходим к важному выводу: нам необходимо либо отказаться от идеи единой транзакции СУБД, либо разделить процесс импорта на независимые этапы. Рассмотрим наиболее эффективные паттерны реализации такой задачи.
Для того чтобы разделить загрузку и тяжелую бизнес-логику, мы применим классический ETL-подход (Extract, Transform, Load). Разберем этот процесс по шагам:
Шаг 1. Многопоточная загрузка в «буфер». Вместо того чтобы сразу создавать сложные бизнес-объекты (документы, справочники) с тяжелыми обработчиками событий, мы загружаем «сырые» данные во временную плоскую таблицу СУБД. В 1С в качестве такого буфера идеально выступает простой РегистрСведений без сложной логики при записи.
На этом этапе мы запускаем параллельные фоновые задания (потоки). Каждый поток берет свою порцию данных и записывает ее в буферный регистр в собственной транзакции. Если один из потоков падает, мы можем легко перезапустить только его порцию, не затрагивая уже импортированные данные.
Шаг 2. Контролируемый однопоточный перенос. Когда все фоновые задания успешно завершили импорт в буфер, запускается финальный процесс переноса данных из буфера в целевые справочники и документы. Так как данные уже находятся внутри СУБД, чтение происходит практически мгновенно.
Если на этом шаге происходит критическая ошибка, мы можем запустить компенсирующий скрипт очистки целевых таблиц по уникальному «Идентификатору пакета загрузки» (GUID), который мы присвоили всей сессии импорта. Это гарантирует нам контролируемый откат без удержания долгой транзакции.
В современных конфигурациях 1С нет необходимости писать многопоточность с нуля. Разберем, как использовать для этого механизмы Библиотеки Стандартных Подсистем (БСП). В частности, нам понадобится метод ДлительныеОперации.ВыполнитьПроцедуруВНесколькоПотоков().
Важное ограничение: Нельзя передавать исходную таблицу в 1 млн строк напрямую в параметры фонового задания через Временное хранилище. Это вызовет переполнение оперативной памяти сервера приложений. Вместо этого перед запуском потоков мы сохраняем исходные данные во внешний файл на сервере (например, CSV или JSON) или предварительно записываем их в базу данных, а в потоки передаем только индексы строк или смещения (офсеты) файлов, которые этот поток должен обработать.
Проанализируем пример кода для инициализации многопоточной загрузки:
// Пример подготовки параметров и запуска многопоточности через БСП
ПараметрыВыполнения = ДлительныеОперации.ПараметрыВыполненияВНесколькоПотоков();
ПараметрыВыполнения.НаименованиеФоновогоЗадания = "ИмпортБольшогоМассиваДанных";
// Формируем параметры для каждого потока (передаем только инструкции, а не сами данные)
МассивПараметров = Новый Массив;
РазмерПорции = 10000; // Оптимальный размер пакета для одной транзакции
ВсегоСтрок = 1000000;
ТекущийИндекс = 0;
Пока ТекущийИндекс < ВсегоСтрок Цикл
ПараметрыПотока = Новый Структура;
ПараметрыПотока.Вставить("ПутьКФайлуДанных", "/temp/import_data.json");
ПараметрыПотока.Вставить("НачальныйИндекс", ТекущийИндекс);
ПараметрыПотока.Вставить("КоличествоСтрок", РазмерПорции);
ПараметрыПотока.Вставить("ИдентификаторПакета", ПараметрыЗагрузки.ИдентификаторПакета);
МассивПараметров.Добавить(ПараметрыПотока);
ТекущийИндекс = ТекущийИндекс + РазмерПорции;
КонецЦикла;
// Запуск потоков
РезультатВыполнения = ДлительныеОперации.ВыполнитьПроцедуруВНесколькоПотоков(
"ЗагрузкаДанныхСервер.ВыполнитьИмпортПорции",
МассивПараметров,
ПараметрыВыполнения
);
Если бизнес-требования обязывают производить запись непосредственно в целевые объекты параллельно, но при этом сохранять возможность отката всего миллиона строк при сбое в любом потоке, классический механизм транзакций СУБД заменяют паттерном Saga (Компенсирующие транзакции).
Суть паттерна состоит в следующем:
Этот подход гарантирует целостность бизнес-данных без риска заблокировать всю СУБД долгоживущей транзакцией.
При параллельной записи данных в несколько потоков неизбежно возникает риск взаимных блокировок (Deadlocks). Это происходит, когда первый поток заблокировал Объект А и ждет Объект Б, а второй поток заблокировал Объект Б и ждет Объект А. Чтобы этого избежать, мы должны применять следующие правила проектирования:
1. Сегментация данных по хэшу. Перед распределением порций по потокам мы должны гарантировать, что разные потоки никогда не будут обновлять одни и те же элементы данных. Для этого распределяйте элементы по потокам на основе хэш-функции от их уникального идентификатора (GUID) или по алфавитному признаку ключевых полей.
2. Оптимистическая блокировка. Платформа 1С:Предприятие 8 использует встроенное версионирование объектов СУБД через служебное поле _Version. Если один поток пытается записать объект, который за долю секунды до этого обновил другой поток, платформа сгенерирует исключение «Данные были изменены или удалены другим пользователем».
Мы должны обрабатывать эту ситуацию в коде фонового задания, перехватывая исключение и совершая повторную попытку записи (Retry Policy) после повторного считывания объекта:
// Пример обработки конфликта версионирования при параллельной записи
ПопыткаЗаписать = Истина;
КоличествоПопыток = 0;
МаксимальноеКоличествоПопыток = 3;
Пока ПопыткаЗаписать И КоличествоПопыток < МаксимальноеКоличествоПопыток Цикл
Попытка
ОбъектДляЗаписи = СсылкаНаОбъект.ПолучитьОбъект();
// Производим необходимые изменения объекта
ОбъектДляЗаписи.Комментарий = "Обновлено при многопоточной загрузке";
ОбъектДляЗаписи.Записать();
ПопыткаЗаписать = Ложь; // Запись успешна, выходим из цикла
Исключение
ИнформацияОбОшибке = ИнформацияОбОшибке();
ТекстОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке);
Если Найти(ТекстОшибки, "Данные были изменены") > 0 Тогда
КоличествоПопыток = КоличествоПопыток + 1;
// Делаем небольшую паузу перед повторной попыткой
Приостановить(1);
Иначе
// Если ошибка не связана с блокировкой версии, прерываем процесс
ВызватьИсключение ТекстОшибки;
КонецЕсли;
КонецПопытки;
КонецЦикла;
Для организации надежной и быстрой загрузки миллионов строк в 1С мы должны отказаться от монолитных транзакций СУБД в пользу распределенной архитектуры. Разделение импорта на этап наполнения буферных таблиц и этап последующей отложенной обработки, использование БСП для распределения потоков и реализация компенсирующих транзакций (паттерн Saga) позволяют достичь максимальной производительности СУБД без ущерба для стабильности системы и целостности данных.