Как оптимизировать работу с планами обмена в 1С: обход блокировок, выборка изменений и предотвращение Race Condition

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

Работа с механизмами регистрации изменений в 1С — одна из самых ответственных и сложных задач при интеграции систем. Ошибки здесь приводят либо к критическому падению производительности пользователей (из-за избыточных блокировок базы данных), либо к потере измененных данных в процессе выгрузки (из-за эффекта "гонки данных" или race condition). В этой статье мы подробно разберем, как построить надежный, быстрый и безопасный механизм обмена данными, избегая классических архитектурных ловушек.

Постановка задачи и основные вопросы

Рассмотрим классический интеграционный сценарий: у нас есть информационная база 1С на СУБД MS SQL, из которой требуется выгружать измененную номенклатуру вместе с актуальными ценами во внешнюю базу данных (например, на SQL-витрину или в BI-систему). Перед нами встают три фундаментальных вопроса:

  1. Момент регистрации изменений: когда и по какому событию регистрировать изменения номенклатуры при изменении ее цены?
  2. Оптимальный алгоритм выборки: как прочитать зарегистрированные изменения, обработать их и очистить таблицу изменений без риска пропустить параллельно записываемые пользователями данные?
  3. Транзакционные блокировки: нужно ли использовать управляемые блокировки, можно ли блокировать таблицы изменений и как ведет себя платформа 1С на уровне СУБД при вызове методов регистрации?

---

Вопрос 1: В какой момент регистрировать номенклатуру при изменении цены?

На первый взгляд, простым решением кажется создание подписки на событие проведения документа установки цен (например, УстановкаЦенНоменклатуры). Однако у такого подхода есть серьезные архитектурные минусы:

Правильное решение: регистрировать изменения необходимо через подписку на событие ПриЗаписи регистра сведений, хранящего цены (например, ЦеныНоменклатуры). Именно на уровне записи набора регистра мы со 100% точностью знаем, для какой именно номенклатуры и какая цена изменилась в системе.

Проанализируем пример кода для обработчика подписки на событие ПриЗаписи регистра сведений цен:


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

Важный архитектурный совет: по возможности разделяйте потоки данных. Не нужно отправлять всю тяжелую информацию по номенклатуре (описания, характеристики, реквизиты) при каждом копеечном изменении цены. Лучшая практика — создать два независимых потока (или два узла/плана обмена): один для справочника Номенклатура (регистрируется только при изменении самого элемента справочника), другой — для регистра ЦеныНоменклатуры. База-приемник на стороне SQL свяжет эти таблицы по уникальному идентификатору GUID номенклатуры. Это в десятки раз снизит объем передаваемого трафика.

---

Вопрос 2: Как правильно выбирать и очищать изменения (Борьба с Race Condition)

Рассмотрим стандартный "наивный" алгоритм выгрузки, который часто приводит к ошибкам:

  1. Прочитать изменения из плана обмена.
  2. Выгрузить данные в приемник.
  3. Вызвать очистку изменений (УдалитьРегистрациюИзменений).

В чем кроется смертельная опасность этого алгоритма? Если между шагом 1 (чтение) и шагом 3 (очистка) пользователи запишут в базу новые изменения по тем же элементам номенклатуры, то на шаге 3 мы очистим регистрацию изменений, которые мы еще не выгружали! В итоге данные в приемнике станут неактуальными, а система "забудет", что товар изменился.

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

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

Если в процессе выгрузки (на шаге 4) пользователи изменят номенклатуру, новые изменения запишутся в таблицу регистрации с НомерСообщения = NULL. При удалении на шаге 5 эти новые записи затронуты не будут и благополучно улетят при следующем сеансе обмена. Мы полностью устранили риск потери данных без единой блокировки базы данных!

Посмотрим на программную реализацию этого безопасного алгоритма:


Процедура ВыгрузитьИзмененияБезопасно() Экспорт
    
    УзелОбмена = ПланыОбмена.ИнтеграцияСБД.НайтиПоКоду("SQL_RECEIVER");
    Если НЕ ЗначениеЗаполнено(УзелОбмена) Тогда
        Возврат;
    КонецЕсли;
    
    // Шаг 1. Генерируем новый номер сообщения
    НовыйНомерСообщения = УзелОбмена.НомерОтправленного + 1;
    
    // Шаг 2. Закрепляем этот номер за всеми текущими изменениями (межевание)
    // Платформа проставит НовыйНомерСообщения в таблице изменений там, где было NULL
    ПланыОбмена.ВыбратьИзменения(УзелОбмена, НовыйНомерСообщения);
    
    // Шаг 3. Запросом выбираем данные, помеченные этим номером сообщения
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |   НоменклатураИзменения.Ссылка КАК Товар,
        |   ЕСТЬNULL(ЦеныНоменклатурыСрезПоследних.Цена, 0) КАК Цена
        |ИЗ
        |   Справочник.Номенклатура.Изменения КАК НоменклатураИзменения
        |   ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних КАК ЦеныНоменклатурыСрезПоследних
        |       ПО НоменклатураИзменения.Ссылка = ЦеныНоменклатурыСрезПоследних.Номенклатура
        |ГДЕ
        |   НоменклатураИзменения.Узел = &УзелОбмена
        |   И НоменклатураИзменения.НомерСообщения = &НомерСообщения";
        
    Запрос.УстановитьПараметр("УзелОбмена", УзелОбмена);
    Запрос.УстановитьПараметр("НомерСообщения", НовыйНомерСообщения);
    
    РезультатЗапроса = Запрос.Выполнить();
    Если НЕ РезультатЗапроса.Пустой() Тогда
        
        Выборка = РезультатЗапроса.Выбрать();
        
        НачатьТранзакцию();
        Попытка
            // Шаг 4. Обрабатываем данные и отправляем во внешнюю базу
            Пока Выборка.Следующий() Цикл
                ОтправитьВВнешнююБД(Выборка.Товар, Выборка.Цена);
            КонецЦикла;
            
            // Фиксируем номер отправленного сообщения в узле
            ОбъектУзла = УзелОбмена.ПолучитьОбъект();
            ОбъектУзла.НомерОтправленного = НовыйНомерСообщения;
            ОбъектУзла.Записать();
            
            // Шаг 5. Очищаем только обработанные изменения
            ПланыОбмена.УдалитьРегистрациюИзменений(УзелОбмена, НовыйНомерСообщения);
            
            ЗафиксироватьТранзакцию();
        Исключение
            ОтменитьТранзакцию();
            ЗаписатьЛогОшибки(ОписаниеОшибки());
            ВызватьИсключение; // Пробрасываем ошибку для анализа
        КонецПопытки;
        
    КонецЕсли;
    
КонецПроцедуры

---

Вопрос 3: Почему не работают управляемые блокировки на таблицы изменений?

Проанализируем интереснейший технический нюанс, с которым сталкиваются многие разработчики при попытке реализовать управляемые блокировки на таблицы изменений (такие как Справочник.Номенклатура.Изменения).

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


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

Результат эксперимента: параллельный сеанс без каких-либо проблем и задержек регистрирует изменения через ЗарегистрироватьИзменения()! Блокировка проигнорирована. Почему так происходит?

Разберем внутреннюю механику платформы 1С:

Следовательно, наложение программной управляемой блокировки на пространство "Справочник.Номенклатура.Изменения" абсолютно бесполезно. Платформа запишет изменения в СУБД в обход вашей блокировки.

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

---

Резюме и лучшие практики

Подведем итоги нашего глубокого разбора оптимизации планов обмена в 1С:

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