Как избежать дублирования порядковых номеров при многопоточной записи в справочник 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

При реализации механизмов ускоренной загрузки данных часто используется разделение на фоновые задания. Однако стандартный подход к получению порядкового номера через запрос «МАКСИМУМ(Номер) + 1» в многопоточной среде неизбежно приводит к дублированию. Это происходит из-за того, что несколько параллельных сеансов одновременно считывают одно и то же максимальное значение до того, как один из них успеет зафиксировать новую запись. Разберем подробно, как решить эту проблему, сохраняя высокую скорость работы и соблюдая ограничения по доработке конфигурации.

Проблема «Грязного чтения» и гонки потоков

Проанализируем ситуацию: когда поток №1 выполняет запрос для получения последнего номера, он видит, например, число 100. В этот же микроскопический промежуток времени поток №2 выполняет аналогичный запрос и тоже получает 100. Оба потока пытаются записать новые элементы с номером 101. Выясним причину: в системе 1С по умолчанию (в режиме управляемых блокировок) чтение данных запросом не блокирует эти данные для других потоков. Чтобы избежать этого, нам необходимо внедрить механизмы синхронизации.

Решение 1: Предварительное распределение номеров (Резервирование)

Рассмотрим наиболее эффективный метод для Highload-систем, который позволяет потокам работать практически не блокируя друг друга. Суть метода заключается в том, чтобы вычислить все необходимые номера до момента запуска фоновых заданий.

Посмотрим на алгоритм реализации:

  1. Определяем общее количество записей, которые нужно создать (например, 100 000).
  2. В основном потоке (перед запуском фоновых) один раз получаем текущий максимальный номер в группе.
  3. Разделяем массив данных на порции для фоновых заданий.
  4. Каждому фоновому заданию в параметры передаем «Начальный номер». Например, Задание №1 получает диапазон с 101 по 1000, Задание №2 — с 1001 по 2000.

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

Решение 2: Использование управляемых блокировок (DataLock)

Если заранее распределить номера невозможно (например, данные поступают в потоки динамически), необходимо использовать объект БлокировкаДанных. Разберем, как правильно настроить блокировку внутри транзакции, чтобы выстроить потоки в очередь только на момент получения номера.

Проанализируем пример кода, который должен выполняться в каждом потоке непосредственно перед записью объекта:


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

НачатьТранзакцию();
Попытка
    Блокировка.Заблокировать();
    
    // Запрос для получения МАКСИМУМ(Номер)
    Запрос = Новый Запрос("ВЫБРАТЬ МАКСИМУМ(НомерПоПорядку) КАК МаксНомер ИЗ Справочник.Номенклатура ГДЕ Родитель = &Родитель");
    Запрос.УстановитьПараметр("Родитель", ГруппаДляЗаписи);
    
    Результат = Запрос.Выполнить();
    Выборка = Результат.Выбрать();
    НовыйНомер = ?(Выборка.Следующий(), Выборка.МаксНомер + 1, 1);
    
    // Создание и запись объекта
    НовыйОбъект = Справочники.Номенклатура.СоздатьЭлемент();
    НовыйОбъект.НомерПоПорядку = НовыйНомер;
    // ... заполнение остальных реквизитов ...
    НовыйОбъект.Записать();
    
    ЗафиксироватьТранзакцию();
Исключение
    ОтменитьТранзакцию();
    ВызватьИсключение;
КонецПопытки;

Важный момент: использование исключительной блокировки по конкретному родителю (группе) позволяет разным потокам работать параллельно, если они пишут данные в разные папки справочника.

Решение 3: Технический объект-замок

В условиях, когда базу нельзя менять (нельзя добавлять регистры или константы), мы можем использовать существующий элемент справочника в качестве «семафора». Рассмотрим этот метод подробнее:

  1. Создаем в справочнике один служебный элемент, например, с наименованием «_ТехническийНумератор_».
  2. Каждый поток перед выполнением запроса на получение номера должен вызвать метод Заблокировать() для этого конкретного элемента или наложить на его ссылку управляемую блокировку.
  3. Пока транзакция не завершена, другие потоки, пытающиеся заблокировать этот же элемент, будут ожидать.

Этот метод гарантирует строгую последовательность, но может немного снизить скорость, так как создает «узкое горлышко».

Решение 4: Оптимизация последующего обхода

Автор темы упоминал, что обход после загрузки в один поток не дает ускорения. Выясним причину: скорее всего, запись объектов производится по одному, что порождает 100 000 транзакций. Чтобы ускорить этот процесс, необходимо использовать запись порциями.

Проанализируем стратегию ускорения записи:

Пример групповой обработки:


НачатьТранзакцию();
Счетчик = 0;
Для Каждого СсылкаИзСписка Из МассивНовыхЭлементов Цикл
    Объект = СсылкаИзСписка.ПолучитьОбъект();
    Объект.НомерПоПорядку = ТекущийНомер;
    Объект.Записать();
    
    ТекущийНомер = ТекущийНомер + 1;
    Счетчик = Счетчик + 1;
    
    Если Счетчик % 1000 = 0 Тогда // Фиксация каждые 1000 записей
        ЗафиксироватьТранзакцию();
        НачатьТранзакцию();
    КонецЕсли;
КонецЦикла;
ЗафиксироватьТранзакцию();

Подведение итогов

Для достижения максимальной производительности при записи 100 000 элементов мы рекомендуем использовать Решение 1 (Резервирование номеров). Оно полностью исключает использование блокировок базы данных в процессе работы фоновых заданий, что дает линейный прирост скорости от количества ядер процессора. Если же логика требует динамического получения номера, обязательно используйте БлокировкаДанных внутри транзакции, чтобы гарантировать уникальность порядковых номеров.

← На главную