Почему возникают конфликты управляемых блокировок при использовании разных измерений регистра

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

В практике разработки на платформе 1С:Предприятие 8.3 часто возникает ситуация, когда две транзакции пытаются установить управляемые блокировки на один и тот же регистр, используя разные измерения. На первый взгляд кажется, что если мы блокируем Контейнер №1 в одной транзакции и Ячейку №2 в другой, то конфликта быть не должно, так как объекты разные. Однако система выдает ошибку блокировки. Разберем подробно, почему это происходит, как работает менеджер управляемых блокировок и как правильно проектировать код, чтобы избежать подобных коллизий.

Анализ проблемы: пересечение пространств данных

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

Первая процедура устанавливает разделяемую блокировку (S):


Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ПоложениеКонтейнеров");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Разделяемый;
ЭлементБлокировки.УстановитьЗначение("Ячейка", Контейнер2); // Ошибка в имени переменной в примере, но суть в фильтре по ячейке
Блокировка.Заблокировать();

Вторая процедура устанавливает исключительную блокировку (X):


Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ПоложениеКонтейнеров");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.УстановитьЗначение("Контейнер", Контейнер1);
Блокировка.Заблокировать();

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

Проанализируем это на логическом уровне:

  1. Транзакция №1 говорит: «Я блокирую Ячейку №2 для всех возможных контейнеров».
  2. Транзакция №2 говорит: «Я блокирую Контейнер №1 для всех возможных ячеек».

Существует ли точка, где эти требования пересекаются? Да, это комбинация (Контейнер №1, Ячейка №2). Поскольку вторая транзакция требует исключительного доступа к этой «точке» пространства, а первая уже захватила её в разделяемом режиме, возникает конфликт блокировок. Менеджеру блокировок неважно, существуют ли физические записи с такими значениями в таблице БД в данный момент — он охраняет само пространство данных.

Геометрическая модель блокировки

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

Если мы блокируем только Контейнер №1, мы «закрашиваем» весь вертикальный столбец в таблице. Любая попытка другой транзакции «закрасить» любую строку (ячейку) неизбежно приведет к пересечению на пересечении этого столбца и этой строки.

Важный вывод: управляемая блокировка — это не блокировка конкретной «строчки» в таблице SQL, это блокировка области в многомерном пространстве измерений метаданных.

Особенности работы в файловом режиме

В ходе обсуждения выяснился важный нюанс: поведение системы сильно зависит от режима работы базы (файловый или клиент-серверный). Проанализируем различия:

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

В файловом варианте ситуация иная. Несмотря на то, что управляемые блокировки там декларируются, платформа зачастую применяет механизм эскалации. Как только вы устанавливаете любую управляемую блокировку на пространство регистра, система может заблокировать всю таблицу целиком для обеспечения целостности, так как внутренний движок файловой СУБД не обладает такой гранулярностью блокировок, как полноценный SQL-сервер.

Проанализируем результаты тестов: в файловой базе даже блокировки по разным значениям одного и того же измерения (например, Контейнер1 и Контейнер2) часто конфликтуют, если одна из них исключительная. Это происходит из-за того, что в файловом режиме любая исключительная блокировка фактически становится блокировкой всей таблицы регистра.

Рекомендации по установке блокировок

Чтобы минимизировать количество конфликтов (Deadlocks или Lock Timeouts), придерживаемся следующих правил:

  1. Блокируйте по всем измерениям. По возможности всегда указывайте значения всех ключевых измерений регистра. Если вы блокируете только одно измерение из трех, вы создаете огромную «мертвую зону» для других процессов.
  2. Соблюдайте порядок. Накладывайте фильтры в элементе блокировки в том же порядке, в котором измерения расположены в метаданных регистра. Это помогает менеджеру блокировок быстрее искать пересечения.
  3. Используйте разделяемый режим там, где это возможно. Разделяемые блокировки (S) совместимы друг с другом. Конфликт возникает только при попытке установить исключительную блокировку (X) поверх любой другой или наоборот.
  4. Минимизируйте время транзакции. Не вставляйте длительные вычисления или паузы между вызовом Блокировка.Заблокировать() и окончанием транзакции.

Использование пауз для отладки блокировок

Для воспроизведения конфликтов в процессе разработки часто требуется искусственно задержать выполнение транзакции. Рассмотрим несколько способов реализации процедуры Пауза(), которые обсуждались в теме:

Способ №1: Использование БСП (Библиотека стандартных подсистем)

Это самый «чистый» способ для современных конфигураций. Мы используем метод ожидания завершения фонового задания:


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

Способ №2: Попытка HTTP-соединения

Хитрый способ, основанный на таймауте сетевого соединения. Рассмотрим реализацию:


Процедура Уснуть(КоличествоСекунд)
    Попытка
        // Пытаемся подключиться к несуществующему адресу с заданным таймаутом
        Соединение = Новый HTTPСоединение("127.0.0.0",,,,,КоличествоСекунд);
        Соединение.Получить(Новый HTTPЗапрос());
    Исключение
        // Игнорируем ошибку таймаута
    КонецПопытки;    
КонецПроцедуры

Важно помнить, что пустые циклы (Пока ... КонецЦикла) использовать крайне не рекомендуется, так как они нагружают процессор сервера на 100%.

Выясняем причину через Технологический журнал

Если логика блокировок кажется верной, но конфликты все равно возникают, проанализируем ситуацию с помощью Технологического журнала (ТЖ). Нас интересует событие TLOCK.

В файле лога можно увидеть параметры:

Это позволяет точно определить, по какому именно «измерению» или «ключу» произошло пересечение. Помните, что управляемые блокировки — это полностью логический механизм внутри сервера 1С. Они не видны в консоли MS SQL Server, так как SQL-сервер видит только физические блокировки таблиц (обычно уровня READ COMMITTED), которые платформа накладывает в момент записи набора записей, а не в момент вызова Заблокировать().

Итоги

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

← На главную