При реализации механизмов ускоренной загрузки данных часто используется разделение на фоновые задания. Однако стандартный подход к получению порядкового номера через запрос «МАКСИМУМ(Номер) + 1» в многопоточной среде неизбежно приводит к дублированию. Это происходит из-за того, что несколько параллельных сеансов одновременно считывают одно и то же максимальное значение до того, как один из них успеет зафиксировать новую запись. Разберем подробно, как решить эту проблему, сохраняя высокую скорость работы и соблюдая ограничения по доработке конфигурации.
Проанализируем ситуацию: когда поток №1 выполняет запрос для получения последнего номера, он видит, например, число 100. В этот же микроскопический промежуток времени поток №2 выполняет аналогичный запрос и тоже получает 100. Оба потока пытаются записать новые элементы с номером 101. Выясним причину: в системе 1С по умолчанию (в режиме управляемых блокировок) чтение данных запросом не блокирует эти данные для других потоков. Чтобы избежать этого, нам необходимо внедрить механизмы синхронизации.
Рассмотрим наиболее эффективный метод для Highload-систем, который позволяет потокам работать практически не блокируя друг друга. Суть метода заключается в том, чтобы вычислить все необходимые номера до момента запуска фоновых заданий.
Посмотрим на алгоритм реализации:
Этот способ идеален, так как полностью исключает конкуренцию за ресурс номера во время работы потоков. Программисту достаточно передать начальное значение счетчика в структуру параметров фонового задания.
Если заранее распределить номера невозможно (например, данные поступают в потоки динамически), необходимо использовать объект БлокировкаДанных. Разберем, как правильно настроить блокировку внутри транзакции, чтобы выстроить потоки в очередь только на момент получения номера.
Проанализируем пример кода, который должен выполняться в каждом потоке непосредственно перед записью объекта:
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Справочник.Номенклатура"); // Замените на ваш справочник
ЭлементБлокировки.УстановитьЗначение("Родитель", ГруппаДляЗаписи);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
НачатьТранзакцию();
Попытка
Блокировка.Заблокировать();
// Запрос для получения МАКСИМУМ(Номер)
Запрос = Новый Запрос("ВЫБРАТЬ МАКСИМУМ(НомерПоПорядку) КАК МаксНомер ИЗ Справочник.Номенклатура ГДЕ Родитель = &Родитель");
Запрос.УстановитьПараметр("Родитель", ГруппаДляЗаписи);
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();
НовыйНомер = ?(Выборка.Следующий(), Выборка.МаксНомер + 1, 1);
// Создание и запись объекта
НовыйОбъект = Справочники.Номенклатура.СоздатьЭлемент();
НовыйОбъект.НомерПоПорядку = НовыйНомер;
// ... заполнение остальных реквизитов ...
НовыйОбъект.Записать();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Важный момент: использование исключительной блокировки по конкретному родителю (группе) позволяет разным потокам работать параллельно, если они пишут данные в разные папки справочника.
В условиях, когда базу нельзя менять (нельзя добавлять регистры или константы), мы можем использовать существующий элемент справочника в качестве «семафора». Рассмотрим этот метод подробнее:
Заблокировать() для этого конкретного элемента или наложить на его ссылку управляемую блокировку.Этот метод гарантирует строгую последовательность, но может немного снизить скорость, так как создает «узкое горлышко».
Автор темы упоминал, что обход после загрузки в один поток не дает ускорения. Выясним причину: скорее всего, запись объектов производится по одному, что порождает 100 000 транзакций. Чтобы ускорить этот процесс, необходимо использовать запись порциями.
Проанализируем стратегию ускорения записи:
НомерПоПорядку пустым.Записать(), используем транзакции по 1000–5000 элементов.Пример групповой обработки:
НачатьТранзакцию();
Счетчик = 0;
Для Каждого СсылкаИзСписка Из МассивНовыхЭлементов Цикл
Объект = СсылкаИзСписка.ПолучитьОбъект();
Объект.НомерПоПорядку = ТекущийНомер;
Объект.Записать();
ТекущийНомер = ТекущийНомер + 1;
Счетчик = Счетчик + 1;
Если Счетчик % 1000 = 0 Тогда // Фиксация каждые 1000 записей
ЗафиксироватьТранзакцию();
НачатьТранзакцию();
КонецЕсли;
КонецЦикла;
ЗафиксироватьТранзакцию();
Для достижения максимальной производительности при записи 100 000 элементов мы рекомендуем использовать Решение 1 (Резервирование номеров). Оно полностью исключает использование блокировок базы данных в процессе работы фоновых заданий, что дает линейный прирост скорости от количества ядер процессора. Если же логика требует динамического получения номера, обязательно используйте БлокировкаДанных внутри транзакции, чтобы гарантировать уникальность порядковых номеров.