Как в 1С обрабатывать каждый N-й документ по альтернативному алгоритму в фоновом режиме?

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

В практике разработки на платформе 1С:Предприятие часто возникает задача разделения потока обрабатываемых данных. Типичный пример: основная масса документов должна обрабатываться по стандартному алгоритму (Алгоритм № 1), а каждый $N$-й документ (например, каждый 5-й или 10-й, где $N$ задается пользователем интерактивно) должен проходить по альтернативному пути (Алгоритм № 2). Это может быть необходимо для проведения выборочного аудита, распределения нагрузки по разным серверам, случайного контроля качества заполнения данных или отправки выборки во внешние системы.

На первый взгляд задача кажется тривиальной — достаточно запустить счетчик в цикле. Однако реальная работа в многопоточной среде, использование регламентных заданий, транзакционность и механизмы распределенных баз данных (РИБ) вносят свои жесткие коррективы. В этой статье мы подробно разберем, как правильно спроектировать и реализовать такую систему, начиная от простейших алгоритмов и заканчивая высоконадежными отказоустойчивыми решениями.

---

Вариант 1. Простой локальный счетчик (для простых однопоточных сценариев)

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

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


ВыборкаДокументов = Документы.РеализацияТоваровУслуг.Выбрать(ДатаНачала, ДатаОкончания);
ТекущийСчетчик = 0;
ЧислоN = 5; // Значение, заданное пользователем

Пока ВыборкаДокументов.Следующий() Цикл
    ТекущийСчетчик = ТекущийСчетчик + 1;
    
    Если ТекущийСчетчик = ЧислоN Тогда
        // Сбрасываем счетчик и запускаем альтернативный алгоритм
        ТекущийСчетчик = 0;
        ОбработатьДокументПоАлгоритму2(ВыборкаДокументов.Ссылка);
    Иначе
        // Запускаем стандартный алгоритм
        ОбработатьДокументПоАлгоритму1(ВыборкаДокументов.Ссылка);
    КонецЕсли;
КонецЦикла;

В чем проблема этого подхода? Данный алгоритм работоспособен только внутри одного непрерывного цикла. Если обработка документов выполняется регламентным заданием, которое запускается каждые 15 минут, то при каждом новом запуске фонового задания переменная ТекущийСчетчик будет инициализироваться заново (сбрасываться в 0).

Рассмотрим ситуацию: в первом запуске обработано 3 документа. Счетчик равен 3. Задание завершилось. Во втором запуске пришел еще 1 документ. Система считает его первым в текущем сеансе, хотя физически он является 4-м. В итоге сбивается вся последовательность, и Алгоритм № 2 выполнится не на 5-м документе, а гораздо позже.

---

Вариант 2. Хранение состояния между сеансами через Хранилище общих настроек

Чтобы счетчик не сбрасывался при завершении работы регламентного задания, его текущее значение необходимо сохранять в базе данных. Создавать для одного числового значения новый регистр сведений или константу не всегда целесообразно, так как это требует изменения метаданных конфигурации.

Платформа 1С предлагает элегантное решение — использовать системную таблицу настроек через механизм ХранилищеОбщихНастроек. Мы можем сохранять значение счетчика под определенным ключом, привязав его к системному пользователю.

Разберем этот метод по шагам:


// Определим имя ключа и системного робота
ИмяОбъекта = "РегламентнаяОбработкаДокументов";
ИмяНастройки = "ИндексПоследнегоДокумента";
ИмяПользователя = "#СистемныйРобот";

// Шаг 1. Считываем сохраненное значение счетчика из БД
ТекущийСчетчик = ХранилищеОбщихНастроек.Загрузить(ИмяОбъекта, ИмяНастройки, , ИмяПользователя);
Если ТекущийСчетчик = Неопределено Тогда
    ТекущийСчетчик = 0;
КонецЕсли;

ЧислоN = 5; // Шаг обработки

ВыборкаДокументов = Документы.РеализацияТоваровУслуг.Выбрать(); // Получаем новую порцию данных

Пока ВыборкаДокументов.Следующий() Цикл
    ТекущийСчетчик = ТекущийСчетчик + 1;
    
    // Используем остаток от деления для определения каждого N-го документа
    Если ТекущийСчетчик % ЧислоN = 0 Тогда
        ОбработатьДокументПоАлгоритму2(ВыборкаДокументов.Ссылка);
    Иначе
        ОбработатьДокументПоАлгоритму1(ВыборкаДокументов.Ссылка);
    КонецЕсли;
КонецЦикла;

// Шаг 2. Записываем обновленное значение счетчика обратно в базу данных
ХранилищеОбщихНастроек.Сохранить(ИмяОбъекта, ИмяНастройки, ТекущийСчетчик, , ИмяПользователя);

Этот способ решает проблему сохранения состояния между вызовами фоновых заданий, не захламляя структуру метаданных конфигурации.

---

Вариант 3. Защита от конкурентного доступа (Управляемые блокировки)

Если фоновое задание может быть запущено параллельно в нескольких потоках или вызывается одновременно с действиями других пользователей, возникает классическая проблема параллельного доступа (Race Condition). Два параллельных потока могут одновременно считать из хранилища значение счетчика (например, «3»), каждый обработает по документу, увеличит значение до «4» и запишет его назад. В результате один шаг будет потерян.

Для гарантированной последовательной работы необходимо использовать транзакционный механизм и **управляемые блокировки** платформы 1С. Поскольку заблокировать элемент ХранилищеОбщихНастроек напрямую нельзя, в высоконагруженных многопоточных системах для хранения счетчика рекомендуется использовать независимый регистр сведений (например, РегистрСведений.СчетчикиОбработки).

Посмотрим на пример правильной блокировки данных в транзакции:


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

НачатьТранзакцию();
Попытка
    // Устанавливаем блокировку. Другие потоки будут ждать завершения нашей транзакции
    Блокировка.Заблокировать();
    
    // Считываем текущее значение счетчика из регистра сведений
    Отбор = Новый Структура("ИмяСчетчика", "РеализацияТоваровУслуг");
    ТекущееЗначение = РегистрыСведений.СчетчикиОбработки.ПолучитьПоследнее(ПроцентЗавершения, Отбор).Значение;
    
    // ... Выполняем обработку документов и увеличиваем счетчик ...
    
    // Записываем новое значение в регистр
    МенеджерЗаписи = РегистрыСведений.СчетчикиОбработки.СоздатьМенеджерЗаписи();
    МенеджерЗаписи.ИмяСчетчика = "РеализацияТоваровУслуг";
    МенеджерЗаписи.Значение = НовоеЗначениеСчетчика;
    МенеджерЗаписи.Записать();
    
    ЗафиксироватьТранзакцию();
Исключение
    ОтменитьТранзакцию();
    ЗаписьЖурналаРегистрации("ОбработкаДокументов", УровеньЖурналаРегистрации.Ошибка, , , ОписаниеОшибки());
КонецПопытки;

---

Вариант 4. Статистический безсерверный метод (Хеш по УИД)

Если жесткая сквозная последовательность (ровно каждый 5-й документ секунда в секунду) не является критическим требованием, а вашей целью является равномерное распределение нагрузки или случайный аудит примерно 20% документов, существует гениальное и максимально производительное решение, не требующее никаких транзакций, блокировок и хранения состояния.

Это **метод вычисления остатка от деления уникального идентификатора (GUID)** документа. У каждого объекта в 1С есть уникальный 36-символьный идентификатор. Так как GUID генерируется случайным образом, распределение его значений подчиняется законам математической статистики. Мы можем использовать это свойство.

В современных версиях платформы 1С (начиная с 8.3.22) получить уникальный идентификатор ссылки можно прямо в запросе с помощью функции УНИКАЛЬНЫЙИДЕНТИФИКАТОР(Ссылка).

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


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |   Реализация.Ссылка КАК Ссылка,
    |   УНИКАЛЬНЫЙИДЕНТИФИКАТОР(Реализация.Ссылка) КАК УИДСтрока
    |ИЗ
    |   Документ.РеализацияТоваровУслуг КАК Реализация
    |ГДЕ
    |   Реализация.Проведен И НЕ Реализация.ПометкаУдаления";

Выборка = Запрос.Выполнить().Выбрать();

ЧислоN = 5; // Нужен каждый 5-й документ (то есть ~20% от общего объема)

Пока Выборка.Следующий() Цикл
    // Получаем последний символ из строкового представления GUID (например, "f")
    ПоследнийСимвол = Прав(ВыборкаУИДСтрока, 1);
    
    // Переводим шестнадцатеричный символ в число от 0 до 15
    ЧисловоеЗначение = Найти("0123456789abcdef", НРег(ПоследнийСимвол)) - 1;
    
    // Проверяем условие деления
    Если ЧисловоеЗначение % ЧислоN = 0 Тогда
        ОбработатьДокументПоАлгоритму2(Выборка.Ссылка);
    Else
        ОбработатьДокументПоАлгоритму1(Выборка.Ссылка);
    КонецЕсли;
КонецЦикла;

Преимущества метода:

  1. Абсолютная масштабируемость: Нет обращений к СУБД для чтения и записи счетчиков, нет никаких блокировок. Программа работает с максимальной скоростью.
  2. Стабильность в РИБ (распределенных базах): Документ с конкретным GUID всегда, на любом узле распределенной базы данных, при любых повторных обменах и перепроведениях будет обрабатываться по одному и тому же алгоритму.
  3. Независимость от порядка обработки: Неважно, в каком порядке документы поступают на обработку — результат деления GUID всегда константен.

---

Вариант 5. Очередь задач на планах обмена (для систем с интеграцией и РИБ)

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

Наиболее надежный промышленный паттерн для этой задачи — **«Очередь задач на планах обмена»**:

  1. Создаем новый технический План обмена (например, ОчередьОбработкиДокументов). В состав плана обмена включаем те документы, которые подлежат обработке.
  2. При записи документа (включая загрузку по РИБ или изменение пользователем) платформа автоматически регистрирует этот документ на узле нашего технического плана обмена.
  3. Регламентное задание считывает накопленные изменения порциями:
    
    УзелОтбора = ПланыОбмена.ОчередьОбработкиДокументов.НайтиПоКоду("СлужебныйУзел");
    ВыборкаИзменений = ПланыОбмена.ВыбратьИзменения(УзелОтбора, НомерСообщения);
    
  4. Обрабатываем полученные документы по порядку их выборки. Для ведения точного счета используем независимый регистр сведений, где фиксируем уникальный порядковый номер обработки для каждого документа.
  5. После успешной обработки порции документов мы очищаем регистрацию изменений: ПланыОбмена.УдалитьРегистрациюИзменений(УзелОтбора, НомерСообщения);

Этот подход гарантирует, что ни один измененный или вновь созданный документ не будет пропущен, а повторная обработка не нарушит логическую последовательность распределения алгоритмов.

---

Резюме: как выбрать оптимальный вариант?

Для правильного выбора архитектурного решения сопоставим рассмотренные методы:

← На главную