Как устранить ошибку блокировки узла плана обмена при записи сообщения?

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

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

Почему возникает блокировка при вызове НачатьЗапись?

Для начала проанализируем внутреннюю логику работы платформы 1С. Метод НачатьЗапись не просто подготавливает XML-структуру. Его главная задача — зафиксировать номер исходящего сообщения для конкретного узла плана обмена. Чтобы гарантировать уникальность и последовательность номеров сообщений, платформа устанавливает исключительную блокировку на запись в таблице узлов плана обмена, соответствующую нашему УзелОбмена.

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

Решение 1: Использование управляемых блокировок (Программный семафор)

Самый правильный способ с точки зрения платформы 8.3 — это использование объекта БлокировкаДанных. Вместо того чтобы ждать, пока НачатьЗапись «упадет» с ошибкой, мы можем явно попытаться заблокировать узел перед началом всех операций. Рассмотрим код реализации:


// Создаем объект блокировки
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("ПланОбмена.УниверсальныйОбмен.Узел");
ЭлементБлокировки.УстановитьЗначение("Ссылка", УзелОбмена);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;

Попытка
    // Пытаемся установить блокировку
    Блокировка.Заблокировать();
    
    // Если мы здесь, значит узел наш, можем начинать запись
    ЗаписьСообщения = ПланыОбмена.СоздатьЗаписьСообщения();
    ЗаписьСообщения.НачатьЗапись(ЗаписьXML, УзелОбмена);
    // ... логика выгрузки данных ...
    ЗаписьСообщения.ЗавершитьЗапись();
Исключение
    // Сюда попадем, если узел уже занят другим процессом
    ЗаписьЖурналаРегистрации("ОбменДанными", УровеньЖурналаРегистрации.Предупреждение, , , 
        "Узел " + УзелОбмена + " заблокирован другим сеансом.");
КонецПопытки;

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

Решение 2: Диспетчеризация через фоновые задания

Разберем ситуацию, когда вызовы веб-сервиса происходят слишком часто. Вместо того чтобы выполнять тяжелый обмен непосредственно в теле метода веб-сервиса, мы можем использовать механизм фоновых заданий. Это позволит нам контролировать количество одновременно работающих процессов для одного узла.

Проанализируем алгоритм работы диспетчера:

  1. При поступлении запроса веб-сервис проверяет, нет ли уже запущенного фонового задания по данному узлу.
  2. Для этого используется метод ФоновыеЗадания.ПолучитьФоновыеЗадания() с фильтром по ключу. В качестве ключа удобно использовать строковое представление ссылки узла.
  3. Если задание уже выполняется, мы либо возвращаем ответ «В процессе», либо просто фиксируем необходимость обмена в регистре сведений.

ПараметрыОтбора = Новый Структура;
ПараметрыОтбора.Вставить("Ключ", "Обмен_" + УзелОбмена.УникальныйИдентификатор());
ПараметрыОтбора.Вставить("Состояние", СостояниеФоновогоЗадания.Активно);

АктивныеЗадания = ФоновыеЗадания.ПолучитьФоновыеЗадания(ПараметрыОтбора);

Если АктивныеЗадания.Количество() = 0 Тогда
    // Запускаем новое задание, так как старых нет
    ФоновыеЗадания.Выполнить("ОбменДаннымиСервер.ВыполнитьВыгрузку", 
        Новый Массив(УзелОбмена), 
        ПараметрыОтбора.Ключ, 
        "Обмен данными с " + УзелОбмена);
КонецЕсли;

Решение 3: Реализация очереди через вспомогательный регистр (Mutex)

Если нам нужно жестко упорядочить вызовы, мы можем создать регистр сведений ОчередьОбмена. Рассмотрим, как это поможет нам в управлении потоками:

При каждом обращении к обмену мы записываем в этот регистр новую запись с текущей датой и ссылкой на узел. Затем мы выбираем из этого регистра самую старую необработанную запись. Таким образом, даже если 10 сеансов одновременно обратятся к системе, они будут выстроены в логическую очередь.

Важный момент: Чтобы избежать коллизий при чтении самой очереди, чтение и отметка о начале обработки должны происходить в одной транзакции с установкой блокировки на запись регистра.

Решение 4: Стратегия повторных попыток (Retry Policy)

В некоторых случаях блокировка носит кратковременный характер. Например, один процесс уже почти закончил ЗавершитьЗапись(), а второй только начал. Чтобы не прерывать работу пользователя ошибкой, применим цикл с ожиданием. Посмотрим на пример реализации:


МаксимальноеЧислоПопыток = 5;
ТекущаяПопытка = 1;
Успешно = Ложь;

Пока ТекущаяПопытка <= МаксимальноеЧислоПопыток И НЕ Успешно Цикл
    Попытка
        НачатьТранзакцию();
        ЗаписьСообщения = ПланыОбмена.СоздатьЗаписьСообщения();
        ЗаписьСообщения.НачатьЗапись(ЗаписьXML, УзелОбмена);
        // ... Логика обмена ...
        ЗаписьСообщения.ЗавершитьЗапись();
        ЗафиксироватьТранзакцию();
        Успешно = Истина;
    Исключение
        ОтменитьТранзакцию();
        ТекущаяПопытка = ТекущаяПопытка + 1;
        Если ТекущаяПопытка <= МаксимальноеЧислоПопыток Тогда
            // Приостанавливаем выполнение на 1-2 секунды перед повтором
            ОбщегоНазначения.Пауза(2); 
        Иначе
            ВызватьИсключение "Не удалось выполнить обмен после " + МаксимальноеЧислоПопыток + " попыток.";
        КонецЕсли;
    КонецПопытки;
КонецЦикла;

Рекомендации по оптимизации транзакций

Выясним причину, по которой узел удерживается слишком долго. Часто разработчики совершают ошибку, помещая длительное формирование тела XML-сообщения внутрь транзакции между НачатьЗапись и ЗавершитьЗапись.

Рассмотрим правильный подход:

  1. Сначала подготовьте все данные, которые планируете выгружать (соберите их в массив или таблицу значений).
  2. Только после того, как все данные собраны, открывайте ЗаписьXML и вызывайте НачатьЗапись.
  3. Быстро запишите данные в XML и закройте сообщение.

Чем меньше времени проходит между вызовами НачатьЗапись и ЗавершитьЗапись, тем меньше вероятность, что другие сеансы столкнутся с ошибкой блокировки объекта. Помните, что НачатьЗапись блокирует узел на всё время до момента вызова ЗавершитьЗапись или окончания транзакции.

Таким образом, комбинируя использование БлокировкаДанных для предотвращения конфликтов и механизм фоновых заданий для диспетчеризации, мы можем создать отказоустойчивую систему обмена, которая корректно обрабатывает конкурентные запросы и не «валится» с ошибками блокировок.

← На главную