Разработчики и администраторы высоконагруженных систем на платформе 1С часто сталкиваются с неприятной ситуацией: при массовой автоматической генерации или интенсивной выписке документов (например, сотен или тысяч реализаций в час) оперативное время проведения начинает стремительно «улетать» вперед. Платформа 1С:Предприятие устроена таким образом, что при оперативном проведении каждый следующий документ должен быть записан на временной оси позже предыдущего. Если в одну секунду проводится несколько документов, система искусственно сдвигает время каждого последующего документа на одну секунду вперед относительно текущего значения оперативной отметки.
В результате при интенсивном потоке (например, 2–3 реализации в секунду во время вечерней пиковой отгрузки) буквально за полчаса-час оперативное время уходит к границе суток — 23:59:59. Как только этот предел достигнут, проведение новых документов в оперативном режиме блокируется с ошибкой, так как платформа не может перенести дату документа на следующий день без ведома пользователя. Давайте подробно разберем, почему возникает эта проблема, как устроен механизм оперативной отметки времени и какие архитектурные подходы позволяют полностью решить эту проблему.
Для начала проанализируем, как работает механизм оперативной отметки времени в платформе 1С:Предприятие 8. Когда пользователь или фоновое задание проводит документ оперативно, платформа обращается к серверному кэшу за значением оперативной отметки времени. Логика работы этого механизма следующая:
rphost как последняя выданная оперативная отметка.Если мы проводим, к примеру, 3600 документов за 10 минут (6 документов в секунду), оперативное время сдвинется вперед на 3600 секунд (на 1 час) относительно реального времени. При суточном объеме в 40 000 накладных оперативное время гарантированно упрется в лимит суток 23:59:59 задолго до окончания реального рабочего дня.
Важная деталь: этот кэш хранится в оперативной памяти сервера приложений 1С. Даже если вы отмените проведение документов или удалите их, сервер 1С все равно будет помнить последнюю выданную отметку 23:59:59. Сбросить этот кэш до наступления реальных календарных суток можно только полной очисткой сеансов (база должна стать абсолютно пустой) или перезапуском службы сервера 1С (рабочих процессов rphost).
---
Самым надежным и современным способом решения этой проблемы является полный отказ от использования платформенного механизма оперативного проведения для массово создаваемых документов. Вместо этого мы переводим проведение документов в неоперативный режим, но реализуем строгий контроль остатков непосредственно в коде транзакции проведения (так называемая «новая методика проведения»).
При неоперативном проведении платформа не сдвигает время вперед на секунду. Документы записываются реальным временем сервера, и в одну и ту же секунду база данных может без проблем записать хоть 100, хоть 1000 документов. Порядок списания остатков регулируется транзакционными блокировками СУБД, а не временем на оси.
Рассмотрим по шагам, как переписать логику проведения на примере условного документа реализации:
Шаг 1. В обработчике ОбработкаПроведения модуля документа принудительно отключаем привязку к режиму проведения для алгоритма контроля остатков. Проверку остатков мы будем делать всегда, независимо от того, оперативно проводится документ или нет.
Шаг 2. Записываем движения по регистрам «вслепую» (без предварительного контроля).
Шаг 3. Устанавливаем управляемую блокировку на те ресурсы регистров, по которым проверяются остатки, чтобы параллельные транзакции не прочитали неактуальные данные.
Шаг 4. Делаем запрос к регистру накопления и проверяем, не ушли ли остатки в минус после записи наших движений. Если обнаружен минус — взводим флаг Отказ = Истина.
Посмотрим на пример реализации такого кода в модуле документа:
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
// Очищаем движения перед записью новых
Движения.ТоварыНаСкладах.Записывать = Истина;
Движения.ТоварыНаСкладах.Очистить();
// 1. Формируем движения "вслепую" на основании табличной части
Для Каждого СтрокаТовара Из Товары Цикл
Движение = Движения.ТоварыНаСкладах.ДобавитьРасход();
Движение.Период = Дата;
Движение.Номенклатура = СтрокаТовара.Номенклатура;
Движение.Склад = Склад;
Движение.Количество = СтрокаТовара.Количество;
КонецЦикла;
// Записываем движения в базу данных, чтобы они отразились в регистре
Движения.Записать();
// 2. Устанавливаем управляемую блокировку для обеспечения целостности данных
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ТоварыНаСкладах");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.УстановитьПараметр("Склад", Склад);
ЭлементБлокировки.ИсточникДанных = Товары;
ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Номенклатура", "Номенклатура");
Блокировка.Заблокировать();
// 3. Выполняем запрос для контроля отрицательных остатков
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТоварыНаСкладахОстатки.Номенклатура КАК Номенклатура,
| ТоварыНаСкладахОстатки.КоличествоОстаток КАК Остаток
|ИЗ
| РегистрНакопления.ТоварыНаСкладах.Остатки(
| &МоментВремени,
| Склад = &Склад
| И Номенклатура В (ВЫБРАТЬ РАЗЛИЧНЫЕ Номенклатура ИЗ &ТаблицаТоваров)) КАК ТоварыНаСкладахОстатки
|ГДЕ
| ТоварыНаСкладахОстатки.КоличествоОстаток < 0";
Запрос.УстановитьПараметр("МоментВремени", Новый Граница(МоментВремени(), ГраницаТипа.Включая));
Запрос.УстановитьПараметр("Склад", Склад);
Запрос.УстановитьПараметр("ТаблицаТоваров", Товары.ВыгрузитьКолонку("Номенклатура"));
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
ОбщегоНазначенияКлиентСервер.СообщитьПользователю(
НСтр("ru = 'Недостаточно товара на складе: '") + Выборка.Номенклатура + " (нехватка: " + (-Выборка.Остаток) + ")");
Отказ = Истина;
КонецЦикла;
КонецЕсли;
КонецПроцедуры
Благодаря этому подходу мы полностью избавляемся от привязки к оперативной отметке времени платформы. Мы можем проводить неограниченное количество документов одной секундой, сохраняя при этом жесткий контроль остатков.
---
Иногда при отказе от оперативного проведения возникает сопутствующая проблема: теряется строгая хронологическая последовательность документов внутри одной секунды. Платформа 1С при одинаковом времени документов сортирует их по внутреннему идентификатору (GUID) ссылки, что не отражает реальный порядок проведения документов пользователями.
Если для бизнес-логики критически важно знать точную хронологию создания и проведения документов внутри секунды, выполним следующие действия:
ТочнаяДатаЗаписи с типом Число (15, 0) или Дата (с точностью до миллисекунд).ПередЗаписью будем фиксировать реальное системное время сервера в миллисекундах.Проанализируем пример кода для заполнения точной даты:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
Если ЭтоНовый() Тогда
// Фиксируем точное универсальное время сервера в миллисекундах
ТочнаяДатаЗаписи = ТекущаяУниверсальнаяДатаВМиллисекундах();
КонецЕсли;
КонецПроцедуры
В дальнейшем при построении отчетов, анализе последовательности или восстановлении хронологии списания партий мы можем выполнять упорядочивание в запросах сначала по стандартному реквизиту Дата, а затем по нашему реквизиту ТочнаяДатаЗаписи. Это гарантирует абсолютную точность определения порядка документов даже при записи сотен документов в одну секунду.
---
Если вам приходится работать со старыми типовыми конфигурациями, где переписать проведение на новую методику слишком трудоемко или невозможно из-за требований поддержки, можно применить обходной маневр с распределением времени документов назад на этапе их программной генерации.
Выясним причину ухода времени вперед: это происходит из-за того, что документы записываются «текущим» временем, которое из-за сдвигов улетает вперед. Но если мы будем записывать и проводить документы программно не текущим временем, а распределять их внутри текущего часа назад (например, заполняя свободные секунды в прошедшем времени текущих суток), то оперативная отметка не пострадает.
Рассмотрим алгоритм распределения пакета документов:
// Пример распределения пакета из 1000 документов внутри текущего дня
Процедура ЗаписатьПакетДокументов(МассивДокументов)
ТекущаяДатаСервера = ТекущаяДата();
// Начнем заполнение, например, с начала текущего часа или сдвигаемся на пару часов назад
СтартовоеВремя = НачалоЧаса(ТекущаяДатаСервера);
Для Индекс = 0 По МассивДокументов.В Граница() Цикл
ДокОбъект = МассивДокументов[Индекс].ПолучитьОбъект();
// Рассчитываем смещение: например, каждые 2 документа записываем в одну секунду
СмещениеСекунд = Цел(Индекс / 2);
НовоеВремя = СтартовоеВремя + СмещениеСекунд;
// Контролируем, чтобы не выйти за рамки текущего времени сервера
Если НовоеВремя >= ТекущаяДатаСервера Тогда
НовоеВремя = ТекущаяДатаСервера - 1;
КонецЕсли;
ДокОбъект.Дата = НовоеВремя;
// Проводим неоперативно, чтобы платформа не сдвигала оперативную отметку сервера!
ДокОбъект.Записать(РежимЗаписиДокумента.Проведение, РежимПроведенияДокумента.Неоперативный);
КонецЦикла;
КонецПроцедуры
При таком подходе документы ложатся на ось времени плотно, равномерно заполняя уже прошедшие секунды дня. Оперативное время сервера при этом остается равным реальному астрономическому времени и не улетает к концу суток.
---
Для восстановления правильной хронологии списания активов и партионного учета платформа 1С предоставляет встроенный механизм последовательностей документов. Если отказаться от оперативного проведения, проведение документов в одну и ту же секунду может нарушить логику списания партий (особенно если документы вводятся задним числом или проводятся параллельно).
Задействовав механизм последовательностей, мы можем:
Это классический паттерн проектирования высоконагруженных ERP-систем, который позволяет разделить быструю транзакционную работу пользователей по выписке документов и тяжелые учетные расчеты по распределению партий и себестоимости.
---
Подводя итоги нашего анализа, выделим ключевые рекомендации для победы над лимитом оперативного проведения в 1С:
ТочнаяДатаЗаписи с фиксацией значения через ТекущаяУниверсальнаяДатаВМиллисекундах().