В практике разработки на платформе 1С:Предприятие часто возникает задача разделения потока обрабатываемых данных. Типичный пример: основная масса документов должна обрабатываться по стандартному алгоритму (Алгоритм № 1), а каждый $N$-й документ (например, каждый 5-й или 10-й, где $N$ задается пользователем интерактивно) должен проходить по альтернативному пути (Алгоритм № 2). Это может быть необходимо для проведения выборочного аудита, распределения нагрузки по разным серверам, случайного контроля качества заполнения данных или отправки выборки во внешние системы.
На первый взгляд задача кажется тривиальной — достаточно запустить счетчик в цикле. Однако реальная работа в многопоточной среде, использование регламентных заданий, транзакционность и механизмы распределенных баз данных (РИБ) вносят свои жесткие коррективы. В этой статье мы подробно разберем, как правильно спроектировать и реализовать такую систему, начиная от простейших алгоритмов и заканчивая высоконадежными отказоустойчивыми решениями.
---
Если вся процедура обработки гарантированно выполняется в рамках одного сеанса (например, пользователь вручную запустил обработку, которая за один проход обрабатывает все накопленные документы), мы можем обойтись обычной локальной переменной-счетчиком.
Проанализируем пример такого кода:
ВыборкаДокументов = Документы.РеализацияТоваровУслуг.Выбрать(ДатаНачала, ДатаОкончания);
ТекущийСчетчик = 0;
ЧислоN = 5; // Значение, заданное пользователем
Пока ВыборкаДокументов.Следующий() Цикл
ТекущийСчетчик = ТекущийСчетчик + 1;
Если ТекущийСчетчик = ЧислоN Тогда
// Сбрасываем счетчик и запускаем альтернативный алгоритм
ТекущийСчетчик = 0;
ОбработатьДокументПоАлгоритму2(ВыборкаДокументов.Ссылка);
Иначе
// Запускаем стандартный алгоритм
ОбработатьДокументПоАлгоритму1(ВыборкаДокументов.Ссылка);
КонецЕсли;
КонецЦикла;
В чем проблема этого подхода? Данный алгоритм работоспособен только внутри одного непрерывного цикла. Если обработка документов выполняется регламентным заданием, которое запускается каждые 15 минут, то при каждом новом запуске фонового задания переменная ТекущийСчетчик будет инициализироваться заново (сбрасываться в 0).
Рассмотрим ситуацию: в первом запуске обработано 3 документа. Счетчик равен 3. Задание завершилось. Во втором запуске пришел еще 1 документ. Система считает его первым в текущем сеансе, хотя физически он является 4-м. В итоге сбивается вся последовательность, и Алгоритм № 2 выполнится не на 5-м документе, а гораздо позже.
---
Чтобы счетчик не сбрасывался при завершении работы регламентного задания, его текущее значение необходимо сохранять в базе данных. Создавать для одного числового значения новый регистр сведений или константу не всегда целесообразно, так как это требует изменения метаданных конфигурации.
Платформа 1С предлагает элегантное решение — использовать системную таблицу настроек через механизм ХранилищеОбщихНастроек. Мы можем сохранять значение счетчика под определенным ключом, привязав его к системному пользователю.
Разберем этот метод по шагам:
// Определим имя ключа и системного робота
ИмяОбъекта = "РегламентнаяОбработкаДокументов";
ИмяНастройки = "ИндексПоследнегоДокумента";
ИмяПользователя = "#СистемныйРобот";
// Шаг 1. Считываем сохраненное значение счетчика из БД
ТекущийСчетчик = ХранилищеОбщихНастроек.Загрузить(ИмяОбъекта, ИмяНастройки, , ИмяПользователя);
Если ТекущийСчетчик = Неопределено Тогда
ТекущийСчетчик = 0;
КонецЕсли;
ЧислоN = 5; // Шаг обработки
ВыборкаДокументов = Документы.РеализацияТоваровУслуг.Выбрать(); // Получаем новую порцию данных
Пока ВыборкаДокументов.Следующий() Цикл
ТекущийСчетчик = ТекущийСчетчик + 1;
// Используем остаток от деления для определения каждого N-го документа
Если ТекущийСчетчик % ЧислоN = 0 Тогда
ОбработатьДокументПоАлгоритму2(ВыборкаДокументов.Ссылка);
Иначе
ОбработатьДокументПоАлгоритму1(ВыборкаДокументов.Ссылка);
КонецЕсли;
КонецЦикла;
// Шаг 2. Записываем обновленное значение счетчика обратно в базу данных
ХранилищеОбщихНастроек.Сохранить(ИмяОбъекта, ИмяНастройки, ТекущийСчетчик, , ИмяПользователя);
Этот способ решает проблему сохранения состояния между вызовами фоновых заданий, не захламляя структуру метаданных конфигурации.
---
Если фоновое задание может быть запущено параллельно в нескольких потоках или вызывается одновременно с действиями других пользователей, возникает классическая проблема параллельного доступа (Race Condition). Два параллельных потока могут одновременно считать из хранилища значение счетчика (например, «3»), каждый обработает по документу, увеличит значение до «4» и запишет его назад. В результате один шаг будет потерян.
Для гарантированной последовательной работы необходимо использовать транзакционный механизм и **управляемые блокировки** платформы 1С. Поскольку заблокировать элемент ХранилищеОбщихНастроек напрямую нельзя, в высоконагруженных многопоточных системах для хранения счетчика рекомендуется использовать независимый регистр сведений (например, РегистрСведений.СчетчикиОбработки).
Посмотрим на пример правильной блокировки данных в транзакции:
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.СчетчикиОбработки");
ЭлементБлокировки.УстановитьЗначение("ИмяСчетчика", "РеализацияТоваровУслуг");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
НачатьТранзакцию();
Попытка
// Устанавливаем блокировку. Другие потоки будут ждать завершения нашей транзакции
Блокировка.Заблокировать();
// Считываем текущее значение счетчика из регистра сведений
Отбор = Новый Структура("ИмяСчетчика", "РеализацияТоваровУслуг");
ТекущееЗначение = РегистрыСведений.СчетчикиОбработки.ПолучитьПоследнее(ПроцентЗавершения, Отбор).Значение;
// ... Выполняем обработку документов и увеличиваем счетчик ...
// Записываем новое значение в регистр
МенеджерЗаписи = РегистрыСведений.СчетчикиОбработки.СоздатьМенеджерЗаписи();
МенеджерЗаписи.ИмяСчетчика = "РеализацияТоваровУслуг";
МенеджерЗаписи.Значение = НовоеЗначениеСчетчика;
МенеджерЗаписи.Записать();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации("ОбработкаДокументов", УровеньЖурналаРегистрации.Ошибка, , , ОписаниеОшибки());
КонецПопытки;
---
Если жесткая сквозная последовательность (ровно каждый 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(Выборка.Ссылка);
КонецЕсли;
КонецЦикла;
Преимущества метода:
---
Если документы могут изменяться задним числом, приходить из других узлов распределенной информационной базы (РИБ) или вводиться пользователями параллельно, обычные счетчики неизбежно дадут сбой. Измененный документ должен быть обработан заново, но при этом он не должен ломать общую цепочку подсчета.
Наиболее надежный промышленный паттерн для этой задачи — **«Очередь задач на планах обмена»**:
ОчередьОбработкиДокументов). В состав плана обмена включаем те документы, которые подлежат обработке.
УзелОтбора = ПланыОбмена.ОчередьОбработкиДокументов.НайтиПоКоду("СлужебныйУзел");
ВыборкаИзменений = ПланыОбмена.ВыбратьИзменения(УзелОтбора, НомерСообщения);
ПланыОбмена.УдалитьРегистрациюИзменений(УзелОтбора, НомерСообщения);
Этот подход гарантирует, что ни один измененный или вновь созданный документ не будет пропущен, а повторная обработка не нарушит логическую последовательность распределения алгоритмов.
---
Для правильного выбора архитектурного решения сопоставим рассмотренные методы: