При построении распределенных информационных систем и интеграции платформы «1С:Предприятие 8» с внешними сервисами (такими как корпоративные порталы, CRM, интернет-магазины или шины данных) разработчики часто сталкиваются со сложной технической задачей. Необходимо обеспечить отправку данных об удалении объектов во внешнюю систему в режиме реального времени. При этом крайне важно гарантировать, что сигнал об удалении уйдет только тогда, когда объект действительно физически удален из базы данных.
Платформа 1С предоставляет стандартные обработчики событий, такие как событие ПередУдалением в модуле объекта или в подписках на события. Однако любой опытный разработчик знает, что данное событие срабатывает до фиксации транзакции в СУБД. Это значит, что если в процессе удаления произойдет какая-либо ошибка (например, сработает контроль ссылочной целостности, сбоит сеть или наложится блокировка базы данных), транзакция будет отменена, а объект останется в базе. Если мы отправим HTTP-запрос или сообщение в брокер непосредственно из обработчика ПередУдалением, внешняя система зафиксирует удаление, которого на самом деле не произошло. Это классическая архитектурная проблема рассинхронизации данных.
В этой статье мы подробно проанализируем, почему стандартные подходы не работают для сценариев «онлайн», разберем паттерн Transactional Outbox, научимся обходить задержки регламентных заданий и рассмотрим альтернативные методы отслеживания удалений с использованием СУБД и Журнала регистрации.
Выясним причину, почему прямая отправка сетевых запросов из транзакционных обработчиков 1С считается грубейшей архитектурной ошибкой. Представим классический сценарий синхронизации:
Номенклатура.ПередУдалением.Попытки решить эту проблему через вызовы интерактивных оповещений на клиенте также обречены на провал, поскольку методы вроде ОписаниеОповещения не могут работать в серверных транзакционных модулях. Следовательно, нам нужен надежный асинхронный механизм, гарантирующий согласованность данных в конечном итоге (Eventual Consistency).
Рассмотрим самый надежный способ интеграции, принятый в современных микросервисных архитектурах — паттерн Transactional Outbox. Его суть заключается в том, что запись о событии удаления производится в ту же самую базу данных и в рамках той же самой транзакции, что и удаление основного объекта. Платформа СУБД гарантирует атомарность: либо запишутся оба изменения (удаление объекта и регистрация события), либо ни одно из них.
Разберем по шагам, как реализовать этот паттерн в 1С:
ИсходящиеСобытияСинхронизации со следующей структурой:
ИдентификаторОбъекта (Тип: Строка или УникальныйИдентификатор)ТипОбъекта (Тип: Строка)ТелоСообщения (Тип: Строка, хранит XML или JSON с реквизитами объекта, которые понадобятся внешней системе, так как после удаления прочитать их из базы будет невозможно)ДатаСобытия (Тип: Дата)ПередУдалением.Посмотрим на пример реализации записи события в транзакционный ящик:
Процедура ПередУдалениемОбъекта(Источник, Отказ) Экспорт
Если Отказ Тогда
Возврат;
КонецЕсли;
// Получаем уникальный идентификатор удаляемого объекта
СсылкаНаОбъект = Источник.Ссылка;
ГУИД = Строка(СсылкаНаОбъект.УникальныйИдентификатор());
ИмяМетаданных = Источник.Метаданные().ПолноеИмя();
// Сериализуем данные, которые понадобятся внешней системе.
// После физического удаления объекта мы не сможем прочитать его реквизиты!
ДанныеОбъекта = Новый Структура;
ДанныеОбъекта.Вставить("Код", Источник.Код);
ДанныеОбъекта.Вставить("Наименование", Источник.Наименование);
// Преобразуем структуру в формат JSON
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, ДанныеОбъекта);
СтрокаДанных = ЗаписьJSON.Закрыть();
// Записываем событие в наш транзакционный исходящий ящик
НаборЗаписей = РегистрыСведений.ИсходящиеСобытияСинхронизации.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.ИдентификаторОбъекта.Установить(ГУИД);
НоваяЗапись = НаборЗаписей.Добавить();
НоваяЗапись.ИдентификаторОбъекта = ГУИД;
НоваяЗапись.ТипОбъекта = ИмяМетаданных;
НоваяЗапись.ТелоСообщения = СтрокаДанных;
НоваяЗапись.ДатаСобытия = ТекущаяДатаСеанса();
// Запись происходит в рамках текущей транзакции удаления
НаборЗаписей.Записать(Истина);
КонецПроцедуры
Если транзакция удаления объекта завершится успешно, запись в регистре ИсходящиеСобытияСинхронизации гарантированно появится. Если произойдет откат транзакции — СУБД автоматически откатит и запись в регистре сведений. Проблема ложных срабатываний полностью решена!
Теперь перед нами стоит задача: как отправить накопленные в регистре ИсходящиеСобытияСинхронизации события во внешнюю систему с минимальной задержкой? Стандартные регламентные задания имеют ограничение на частоту запуска (обычно не чаще, чем раз в несколько секунд), и их частый запуск создает высокую нагрузку на СУБД.
Для реализации «честного онлайн» обмена с минимальным временем отклика мы можем использовать паттерн Демон-пуллер (Daemon Puller). Это фоновое задание, которое запускается один раз при старте системы и работает непрерывно в бесконечном цикле.
Проанализируем пример кода такого фонового процесса:
Процедура ВыполнитьНепрерывнуюОтправку() Экспорт
УстановитьПривилегированныйРежим(Истина);
Пока Истина Цикл
// Выбираем порцию событий из регистра
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 100
| События.ИдентификаторОбъекта КАК ИдентификаторОбъекта,
| События.ТипОбъекта КАК ТипОбъекта,
| События.ТелоСообщения КАК ТелоСообщения
|ИЗ
| РегистрСведений.ИсходящиеСобытияСинхронизации КАК События
|УПОРЯДОЧИТЬ ПО
| События.ДатаСобытия ВОЗР";
РезультатЗапроса = Запрос.Выполнить();
Если РезультатЗапроса.Пустой() Тогда
// Если событий нет, приостанавливаем поток на 200 миллисекунд
// Это предотвращает 100% загрузку процессора холостыми запросами
ПриостановитьВыполнение(0.2);
Продолжить;
КонецЕсли;
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
// Выполняем HTTP-запрос во внешнюю систему
УспешнаяОтправка = ОтправитьЗапросВне(Выборка.ИдентификаторОбъекта, Выборка.ТипОбъекта, Выборка.ТелоСообщения);
Если УспешнаяОтправка Тогда
// Удаляем обработанное событие
Набор = РегистрыСведений.ИсходящиеСобытияСинхронизации.СоздатьНаборЗаписей();
Набор.Отбор.ИдентификаторОбъекта.Установить(Выборка.ИдентификаторОбъекта);
Набор.Записать(Истина);
КонецЕсли;
КонецЦикла;
// Небольшая пауза между итерациями обработки очереди
ПриостановитьВыполнение(0.1);
КонецЦикла;
КонецПроцедуры
Использование метода ПриостановитьВыполнение() (или аналогичных механизмов засыпания потока) позволяет удерживать фоновое задание в активном состоянии без ущерба для производительности сервера. Задержка при передаче событий удаления в таком случае составит не более 100–300 миллисекунд, что полностью удовлетворяет требованиям к системам реального времени.
Платформа 1С имеет мощный встроенный механизм отслеживания изменений — ПланыОбмена. При физическом удалении объекта платформа автоматически регистрирует изменения для всех узлов, входящих в план обмена, сохраняя ссылку на удаленный объект. Особенность заключается в том, что при попытке получить данные по этой ссылке платформа вернет значение ОбъектНеНайден, содержащее исходный GUID.
Преимущество этого подхода в том, что нам не нужно вручную прописывать код в обработчиках ПередУдалением для каждого справочника или документа. Достаточно включить нужные объекты в состав плана обмена.
Разберем по шагам, как организовать отправку через планы обмена:
ОнлайнИнтеграция.Посмотрим на пример обработки зарегистрированных изменений удаления:
Процедура ОбработатьИзмененияПланаОбмена(УзелСсылка) Экспорт
// Получаем выборку изменений для конкретного узла
ВыборкаИзменений = ПланыОбмена.ВыбратьИзменения(УзелСсылка, 100);
Пока ВыборкаИзменений.Следующий() Цикл
ДанныеИзменения = ВыборкаИзменений.Получить();
ТипОбъекта = Метаданные.НайтиПоТипу(ТипЗнч(ДанныеИзменения)).ПолноеИмя();
// Проверяем, удален ли объект из базы данных
Если ДанныеИзменения.ПолучитьОбъект() = Неопределено Тогда
// Объект удален физически, у нас есть только битая ссылка с GUID
Идентификатор = Строка(ДанныеИзменения.УникальныйИдентификатор());
// Отправляем сигнал об удалении во внешнюю систему
Результат = ОтправитьУведомлениеОбУдалении(Идентификатор, ТипОбъекта);
Если Результат Тогда
// Подтверждаем получение изменения для узла
ПланыОбмена.УдалитьРегистрациюИзменений(УзелСсылка, ДанныеИзменения);
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Этот метод чрезвычайно надежен, поддерживается платформой на уровне ядра и не требует дублирования данных в промежуточные регистры сведений.
Если внесение изменений в конфигурацию крайне нежелательно или заблокировано (например, конфигурация находится на поддержке без возможности изменения), мы можем использовать Журнал регистрации платформы 1С.
При успешном удалении любого объекта платформа гарантированно делает запись в Журнале регистрации с событием _$Data$_.Delete. Запись происходит только после успешной фиксации транзакции СУБД, что автоматически исключает проблему ложных срабатываний.
Для реализации этого сценария мы можем написать фоновое задание в 1С, которое периодически считывает новые записи из журнала регистрации с помощью метода ВыгрузитьЖурналРегистрации(). Пример настройки отбора для чтения событий удаления:
ПараметрыОтбора = Новый Структура;
ПараметрыОтбора.Вставить("Событие", "_$Data$_.Delete");
ПараметрыОтбора.Вставить("ДатаНачала", ТекущаяДата() - 60); // за последнюю минуту
ТаблицаЖурнала = Новый ТаблицаЗначений;
ВыгрузитьЖурналРегистрации(ТаблицаЖурнала, ПараметрыОтбора, "Дата, Данные, ДанныеПредставление, Метаданные");
Для Каждого СтрокаЛога Из ТаблицаЖурнала Цикл
// СтрокаЛога.Данные содержит UUID удаленного объекта
// СтрокаЛога.Метаданные содержит имя метаданных объекта
ОтправитьСигналУдаления(СтрокаЛога.Данные, СтрокаЛога.Метаданные.ПолноеИмя());
КонецЦикла;
Этот подход избавляет конфигурацию от лишнего транзакционного кода, но требует регулярного чтения журнала регистрации, что при высокой интенсивности записи может создавать дополнительную нагрузку на дисковую подсистему сервера приложений 1С.
Для высоконагруженных систем корпоративного уровня (Enterprise) изменение кода 1С для целей интеграции может быть узким местом из-за блокировок базы данных. В таких случаях применяется подход Change Data Capture (CDC) на уровне СУБД (например, MS SQL Server, PostgreSQL).
Инструменты класса CDC (такие как Debezium) подключаются непосредственно к бинарным логам транзакций СУБД. Они перехватывают операции DELETE над таблицами справочников и документов 1С. Поскольку СУБД записывает эти операции в логи только после успешной команды COMMIT, интеграционная шина (например, Kafka) мгновенно получает гарантированно актуальные данные о физическом удалении записи.
Этот вариант требует глубокого понимания структуры таблиц базы данных 1С (так как имена таблиц в СУБД генерируются платформой динамически, например, _Reference45) и настройки дополнительного системного ПО, но он обеспечивает максимальное быстродействие и абсолютную независимость от кода платформы 1С.