Как устранить конфликт блокировок в регистре «Кэш программных интерфейсов» при синхронизации УТ и БП

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

Приветствуем вас! Сегодня мы разберем крайне неприятную и сложную техническую проблему, с которой часто сталкиваются специалисты при настройке и эксплуатации синхронизации данных между локальными базами (например, «1С:Управление торговлей», редакция 10.3 на обычном интерфейсе) и облачными решениями в 1С:Fresh (например, «1С:Бухгалтерия предприятия», редакция 3.0).

Ошибка проявляется внезапно во время выполнения обмена данными или даже при простой проверке подключения. Платформа аварийно завершает процесс и выдает системное сообщение СУБД Microsoft SQL Server: Превышено время ожидания запроса на блокировку. HRESULT=80040E31, SQLSrvr: SQLSTATE=HYT00, state=31, Severity=10, native=1222. Давайте вместе разберемся в причинах этого поведения, детально проанализируем проблемный код и найдем надежные способы решения этой задачи.

Выясняем причину конфликта блокировок: архитектурный тупик в БСП

Для начала проанализируем, на какой именно объект базы данных ругается СУБД. В ходе детального рассмотрения технологического журнала и логов обмена выясняется, что конфликт происходит в служебном регистре сведений РегистрСведений.КэшПрограммныхИнтерфейсов. Этот регистр входит в состав Библиотеки стандартных подсистем (БСП) и предназначен для оптимизации работы с внешними веб-сервисами. Вместо того чтобы при каждом обращении запрашивать тяжелые описания WSDL и схемы данных по сети, 1С кэширует их в этом регистре в виде двоичных данных (объектов ХранилищеЗначения).

Но почему же возникает взаимоблокировка или таймаут ожидания СУБД? Давайте подробно разберем логику работы стандартного кода БСП, который отвечает за обновление этого кэша.

Посмотрим на пример кода, выполняющего чтение и последующую запись в регистр:


Запрос = Новый Запрос;
Запрос.Текст =
    "ВЫБРАТЬ
    |    ТаблицаКэша.ДатаОбновления КАК ДатаОбновления,
    |    ТаблицаКэша.Данные КАК Данные,
    |    ТаблицаКэша.ТипДанных КАК ТипДанных
    |ИЗ
    |    РегистрСведений.КэшПрограммныхИнтерфейсов КАК ТаблицаКэша
    |ГДЕ
    |    ТаблицаКэша.Идентификатор = &Идентификатор
    |    И ТаблицаКэша.ТипДанных = &ТипДанных";
Запрос.УстановитьПараметр("Идентификатор", Идентификатор);
Запрос.УстановитьПараметр("ТипДанных", ТипДанных);

НачатьТранзакцию();

ОшибкаУстановкиБлокировки = Ложь;

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

На первый взгляд, код написан по всем правилам обеспечения транзакционной целостности: открывается транзакция с помощью метода НачатьТранзакцию(), устанавливается объектная блокировка ЗаблокироватьДанныеДляРедактирования(), а затем накладывается управляемая блокировка СУБД через объект БлокировкаДанных. Однако здесь кроется серьезная архитектурная ловушка.

Обратите внимание на то, что происходит, когда в базе включено разделение данных (что абсолютно естественно при работе с сервисом 1С:Fresh). В этом случае в свойство набора записей передаются ПараметрыПолучения, а реальная подготовка данных происходит в модуле набора записей регистра в процедуре ПередЗаписью().

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


Процедура ПередЗаписью(Отказ, Замещение)
    
    // Проверка значения свойства ОбменДанными.Загрузка отсутствует...
    
    ТребуетсяКонтроль = ОбщегоНазначенияПовтИсп.РазделениеВключено() И ОбщегоНазначенияПовтИсп.ДоступноИспользованиеРазделенныхДанных();
    
    Если ТребуетсяКонтроль Тогда
        
        ПараметрыПолучения = Неопределено;
        Если Не ДополнительныеСвойства.Свойство("ПараметрыПолучения", ПараметрыПолучения) Тогда
            
            ВызватьИсключение НСтр("ru = 'При записи данных в регистр сведений КэшПрограммныхИнтерфейсов из сеансов 
                                    |с включенным разделением требуется передавать параметры получения данных для кэша
                                    |интерфейсов в дополнительном свойстве набора записей ПараметрыПолучения!'");
            
        КонецЕсли;
        
        Для Каждого Запись Из ЭтотОбъект Цикл
            
            Данные = ОбщегоНазначения.ПодготовитьДанныеКэшаВерсий(
                Запись.ТипДанных, ПараметрыПолучения);
            Запись.Данные = Новый ХранилищеЗначения(Данные);
            
        КонецЦикла;
        
        ТаблицаКонтроля = ЭтотОбъект.Выгрузить();
        
    КонецЕсли;
    
КонецПроцедуры

Что здесь не так? Метод ОбщегоНазначения.ПодготовитьДанныеКэшаВерсий() выполняет полноценный внешний веб-запрос (HTTP или SOAP) к удаленному серверу 1С:Fresh для получения актуальных версий интерфейсов. И этот тяжелый, непредсказуемый по времени сетевой запрос выполняется внутри открытой транзакции СУБД!

Пока выполняется сетевой запрос, СУБД Microsoft SQL Server удерживает монопольные блокировки на строки таблицы регистра сведений КэшПрограммныхИнтерфейсов. Если в этот же момент другое фоновое задание обмена или другой пользователь пытается прочитать данные или обновить кэш, их транзакции выстраиваются в очередь. СУБД ждет по умолчанию 20 секунд (стандартный таймаут ожидания блокировки в 1С), после чего аварийно прерывает транзакцию второго процесса с ошибкой превышения времени ожидания (HRESULT=80040E31).

Влияние сетевых настроек и «проблема 7 секунд»

Ситуация усугубляется внутренними особенностями реализации механизмов веб-сервисов в БСП. При попытке инициализировать подключение, система выполняет так называемый «быстрый пинг» удаленного сервиса. На этот пинг в коде БСП выделяется жестко ограниченное время — всего 7 секунд.

Если сетевой канал между вашим сервером SQL и серверами 1С:Fresh перегружен, или если сервер Фреша в данный момент испытывает повышенную нагрузку, этот 7-секундный интервал истекает. Система ошибочно считает, что текущий сохраненный кэш недействителен, стирает его и инициирует процедуру полного обновления кэша интерфейсов. Это снова запускает тяжелую транзакцию записи с веб-запросом внутри, вызывая лавинообразный рост конфликтов блокировок в многопользовательской среде.

Взаимосвязь с ошибкой сопоставления узлов планов обмена

Часто конфликт блокировок сопровождается еще одной ошибкой: «В базе Бух 3.0 не найдена настройка с узлом УТ». На первый взгляд эти проблемы кажутся несвязанными, однако связь здесь самая прямая.

Когда в одной из баз (в данном случае в Бухгалтерии предприятия 3.0 во Фреше) ломаются или удаляются настройки обмена, узел плана обмена для УТ становится неактивным или некорректным. При каждой автоматической попытке начать синхронизацию по расписанию, система не может выполнить стандартный сеанс обмена. Вместо этого она пытается заново выполнить процедуру инициализации и авторизации на веб-сервисе, что неизбежно приводит к постоянному принудительному обновлению регистра КэшПрограммныхИнтерфейсов. Таким образом, сломанная настройка узла генерирует бесконечный поток транзакций обновления кэша, гарантированно приводя к дедлокам.

Пошаговое руководство по решению проблемы

Для полного устранения данной ошибки и стабилизации процесса обмена данными выполним последовательно следующие шаги.

Шаг 1. Проверка и восстановление настроек планов обмена

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

  1. Откройте базу «1С:Управление торговлей 10.3» и перейдите в настройки синхронизации данных. Проверьте код узла текущей информационной базы и код узла базы-приемника.
  2. Откройте «1С:Бухгалтерию предприятия 3.0» во Фреше. Перейдите в раздел «Администрирование» — «Синхронизация данных» — «Настройки синхронизации».
  3. Убедитесь, что код узла для УТ в Бухгалтерии символьно совпадает с кодом, прописанным в самой УТ. Если узел был случайно удален или изменен, создайте настройку заново, строго сопоставив уникальные коды узлов (псевдонимы узлов).

Шаг 2. Ручная очистка зависшего кэша программных интерфейсов

Иногда блокировка «подвисает» в СУБД из-за некорректно завершенных сеансов. Нам необходимо принудительно очистить таблицу кэша. Это абсолютно безопасная операция, так как кэш автоматически перезапишется при первом же успешном сеансе обмена:

  1. В УТ 10.3 перейдите в меню «Операции» — «Регистры сведений» (или воспользуйтесь поиском «Все функции» / «Функции для технического специалиста» в управляемом интерфейсе БП).
  2. Найдите регистр сведений КэшПрограммныхИнтерфейсов.
  3. Выделите все записи в списке (сочетанием клавиш Ctrl + A) и удалите их.
  4. Выполните аналогичную операцию в облачной базе БП 3.0, если к ней есть административный доступ (либо выполните очистку через специальную обработку удаления данных).

Шаг 3. Рефакторинг кода и вынос сетевых запросов из транзакции (для программистов)

Если вы хотите кардинально решить проблему на уровне конфигурации (например, создав расширение или доработав код конфигурации УТ 10.3, если она снята с поддержки), необходимо исключить выполнение метода ПодготовитьДанныеКэшаВерсий() из транзакции СУБД.

Рассмотрим, как правильно переписать данный участок кода. Вместо того чтобы вызывать сетевой метод внутри транзакции или внутри процедуры ПередЗаписью(), мы должны сначала получить данные по сети, сохранить их в локальную переменную, и лишь затем быстро открыть транзакцию и записать готовые данные в СУБД.

Посмотрим на правильный шаблон реализации:


// Сначала выполняем тяжелый сетевой запрос ВНЕ транзакции
ПараметрыПолучения = ПолучитьПараметрыОбмена(); // Ваша функция получения параметров
ДанныеКэша = Неопределено;

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

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

При таком подходе транзакция СУБД длится доли миллисекунды (простая операция записи готового набора в таблицу), что полностью исключает возможность возникновения таймаутов блокировок (ошибки native=1222) у других пользователей и фоновых процессов.

Мы подробно проанализировали механизм работы кэша программных интерфейсов, выяснили истинную причину возникновения конфликтов блокировок СУБД и связали её со сбоями в сопоставлении узлов планов обмена. Следуя предложенным инструкциям, вы сможете не только оперативно восстановить работоспособность обмена данными, но и оптимизировать логику работы вашей системы для предотвращения подобных ошибок в будущем.

← На главную