Работа с механизмами регистрации изменений в 1С — одна из самых ответственных и сложных задач при интеграции систем. Ошибки здесь приводят либо к критическому падению производительности пользователей (из-за избыточных блокировок базы данных), либо к потере измененных данных в процессе выгрузки (из-за эффекта "гонки данных" или race condition). В этой статье мы подробно разберем, как построить надежный, быстрый и безопасный механизм обмена данными, избегая классических архитектурных ловушек.
Рассмотрим классический интеграционный сценарий: у нас есть информационная база 1С на СУБД MS SQL, из которой требуется выгружать измененную номенклатуру вместе с актуальными ценами во внешнюю базу данных (например, на SQL-витрину или в BI-систему). Перед нами встают три фундаментальных вопроса:
---
На первый взгляд, простым решением кажется создание подписки на событие проведения документа установки цен (например, УстановкаЦенНоменклатуры). Однако у такого подхода есть серьезные архитектурные минусы:
Правильное решение: регистрировать изменения необходимо через подписку на событие ПриЗаписи регистра сведений, хранящего цены (например, ЦеныНоменклатуры). Именно на уровне записи набора регистра мы со 100% точностью знаем, для какой именно номенклатуры и какая цена изменилась в системе.
Проанализируем пример кода для обработчика подписки на событие ПриЗаписи регистра сведений цен:
Процедура ПодпискаНаРегистрЦенПриЗаписи(Источник, Отказ, Замещение) Экспорт
// Источник — это НаборЗаписей регистра сведений
Если Источник.ОбменДанными.Загрузка Тогда
Возврат; // Исключаем зацикливание при загрузке данных
КонецЕсли;
// Получаем узел нашего плана обмена
УзелОбмена = ПланыОбмена.ИнтеграцияСБД.НайтиПоКоду("SQL_RECEIVER");
Если НЕ ЗначениеЗаполнено(УзелОбмена) Тогда
Возврат;
КонецЕсли;
// Собираем уникальный массив номенклатуры, по которой изменились цены
МассивНоменклатуры = Новый Массив;
Для Каждого Запись Из Источник Цикл
Если ЗначениеЗаполнено(Запись.Номенклатура)
И МассивНоменклатуры.Найти(Запись.Номенклатура) = Неопределено Тогда
МассивНоменклатуры.Добавить(Запись.Номенклатура);
КонецЕсли;
КонецЦикла;
// Регистрируем изменения для собранного списка товаров
Если МассивНоменклатуры.Количество() > 0 Тогда
ПланыОбмена.ЗарегистрироватьИзменения(УзелОбмена, МассивНоменклатуры);
КонецЕсли;
КонецПроцедуры
Важный архитектурный совет: по возможности разделяйте потоки данных. Не нужно отправлять всю тяжелую информацию по номенклатуре (описания, характеристики, реквизиты) при каждом копеечном изменении цены. Лучшая практика — создать два независимых потока (или два узла/плана обмена): один для справочника Номенклатура (регистрируется только при изменении самого элемента справочника), другой — для регистра ЦеныНоменклатуры. База-приемник на стороне SQL свяжет эти таблицы по уникальному идентификатору GUID номенклатуры. Это в десятки раз снизит объем передаваемого трафика.
---
Рассмотрим стандартный "наивный" алгоритм выгрузки, который часто приводит к ошибкам:
УдалитьРегистрациюИзменений).В чем кроется смертельная опасность этого алгоритма? Если между шагом 1 (чтение) и шагом 3 (очистка) пользователи запишут в базу новые изменения по тем же элементам номенклатуры, то на шаге 3 мы очистим регистрацию изменений, которые мы еще не выгружали! В итоге данные в приемнике станут неактуальными, а система "забудет", что товар изменился.
Для решения этой проблемы платформа 1С предоставляет механизм номеров сообщений. Рассмотрим правильный, рекомендуемый стандартном БСП (Библиотеки Стандартных Подсистем) алгоритм выгрузки без наложения избыточных блокировок:
ПланыОбмена.ВыбратьИзменения(Узел, НомерСообщения). Обратите внимание: этот метод работает мгновенно, так как он не извлекает сами данные физически. Он лишь проставляет переданный НомерСообщения во все записи таблицы изменений данного узла, где поле номера сообщения было равно NULL (то есть помечает порцию изменений для текущей сессии).НомерСообщения = &НомерСообщения.ПланыОбмена.УдалитьРегистрациюИзменений(Узел, НомерСообщения). Метод удалит из таблицы изменений только те записи, у которых номер сообщения совпадает с переданным.Если в процессе выгрузки (на шаге 4) пользователи изменят номенклатуру, новые изменения запишутся в таблицу регистрации с НомерСообщения = NULL. При удалении на шаге 5 эти новые записи затронуты не будут и благополучно улетят при следующем сеансе обмена. Мы полностью устранили риск потери данных без единой блокировки базы данных!
Посмотрим на программную реализацию этого безопасного алгоритма:
Процедура ВыгрузитьИзмененияБезопасно() Экспорт
УзелОбмена = ПланыОбмена.ИнтеграцияСБД.НайтиПоКоду("SQL_RECEIVER");
Если НЕ ЗначениеЗаполнено(УзелОбмена) Тогда
Возврат;
КонецЕсли;
// Шаг 1. Генерируем новый номер сообщения
НовыйНомерСообщения = УзелОбмена.НомерОтправленного + 1;
// Шаг 2. Закрепляем этот номер за всеми текущими изменениями (межевание)
// Платформа проставит НовыйНомерСообщения в таблице изменений там, где было NULL
ПланыОбмена.ВыбратьИзменения(УзелОбмена, НовыйНомерСообщения);
// Шаг 3. Запросом выбираем данные, помеченные этим номером сообщения
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| НоменклатураИзменения.Ссылка КАК Товар,
| ЕСТЬNULL(ЦеныНоменклатурыСрезПоследних.Цена, 0) КАК Цена
|ИЗ
| Справочник.Номенклатура.Изменения КАК НоменклатураИзменения
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних КАК ЦеныНоменклатурыСрезПоследних
| ПО НоменклатураИзменения.Ссылка = ЦеныНоменклатурыСрезПоследних.Номенклатура
|ГДЕ
| НоменклатураИзменения.Узел = &УзелОбмена
| И НоменклатураИзменения.НомерСообщения = &НомерСообщения";
Запрос.УстановитьПараметр("УзелОбмена", УзелОбмена);
Запрос.УстановитьПараметр("НомерСообщения", НовыйНомерСообщения);
РезультатЗапроса = Запрос.Выполнить();
Если НЕ РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
НачатьТранзакцию();
Попытка
// Шаг 4. Обрабатываем данные и отправляем во внешнюю базу
Пока Выборка.Следующий() Цикл
ОтправитьВВнешнююБД(Выборка.Товар, Выборка.Цена);
КонецЦикла;
// Фиксируем номер отправленного сообщения в узле
ОбъектУзла = УзелОбмена.ПолучитьОбъект();
ОбъектУзла.НомерОтправленного = НовыйНомерСообщения;
ОбъектУзла.Записать();
// Шаг 5. Очищаем только обработанные изменения
ПланыОбмена.УдалитьРегистрациюИзменений(УзелОбмена, НовыйНомерСообщения);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписатьЛогОшибки(ОписаниеОшибки());
ВызватьИсключение; // Пробрасываем ошибку для анализа
КонецПопытки;
КонецЕсли;
КонецПроцедуры
---
Проанализируем интереснейший технический нюанс, с которым сталкиваются многие разработчики при попытке реализовать управляемые блокировки на таблицы изменений (такие как Справочник.Номенклатура.Изменения).
Иногда программисты пытаются принудительно заблокировать таблицу изменений во время выгрузки, чтобы предотвратить запись новых изменений пользователями во время сеанса обмена. Код такой попытки выглядит следующим образом:
НачатьТранзакцию(РежимУправленияБлокировкойДанных.Управляемый);
Попытка
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить();
// Попытка наложить исключительную блокировку на таблицу изменений конкретного товара
ЭлементБлокировки.Область = "Справочник.Номенклатура.Изменения";
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Ссыль = Справочники.Номенклатура.НайтиПоКоду("00000073078");
ЭлементБлокировки.УстановитьЗначение("Ссылка", Ссыль);
Блокировка.Заблокировать();
// В это время в параллельном сеансе выполняется регистрация изменений:
// ПланыОбмена.ЗарегистрироватьИзменения(Узел, Ссыль);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
КонецПопытки;
Результат эксперимента: параллельный сеанс без каких-либо проблем и задержек регистрирует изменения через ЗарегистрироватьИзменения()! Блокировка проигнорирована. Почему так происходит?
Разберем внутреннюю механику платформы 1С:
READ COMMITTED с включенным версионированием строк (RCSI - Read Committed Snapshot Isolation). В этом режиме операции чтения и записи практически не блокируют друг друга на физическом уровне базы данных.ПланыОбмена.ЗарегистрироватьИзменения() и ПланыОбмена.УдалитьРегистрациюИзменений() являются оптимизированными низкоуровневыми платформенными функциями. При их вызове платформа выполняет прямые SQL-запросы к физическим таблицам изменений СУБД (например, _ReferenceChngR), минуя стандартные объектные механизмы 1С. Главное: эти методы не обращаются к менеджеру управляемых блокировок 1С для проверки пространства блокировок таблиц изменений!Следовательно, наложение программной управляемой блокировки на пространство "Справочник.Номенклатура.Изменения" абсолютно бесполезно. Платформа запишет изменения в СУБД в обход вашей блокировки.
Если вы действительно хотите принудительно заблокировать запись во время критически важной транзакции обмена, вам придется накладывать блокировку на основной объект (на сам Справочник.Номенклатура), а не на его таблицу изменений. Однако помните, что блокировка основного справочника приведет к ошибкам ожидания и недовольству пользователей, работающих в системе параллельно с обменом. Намного эффективнее и правильнее использовать механизм номеров сообщений, описанный во втором вопросе.
---
Подведем итоги нашего глубокого разбора оптимизации планов обмена в 1С:
БлокировкаДанных — это снижает производительность и архитектурно неэффективно.ВыбратьИзменения(Узел, НомерСообщения) в сочетании с последующим запросом по конкретному номеру сообщения полностью решает проблему "гонки данных" (Race Condition).ВыбратьИзменения() без параметров на больших таблицах, поскольку СУБД эффективно строит план запроса по кластерному индексу таблицы изменений.ПриЗаписи соответствующего регистра сведений, а не документов их установки.