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

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

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

В этой статье мы подробно разберем, почему возникает это поведение на уровне СУБД MS SQL Server, как на это влияет автоматический режим управления блокировками в 1С и какие архитектурные решения помогут раз и навсегда обеспечить высокую параллельность записи.

Выясняем причину: как СУБД выполняет запись в автоматическом режиме

Если в вашей конфигурации установлен автоматический режим управления блокировками (или база работает в режиме совместимости со старыми версиями платформы, например, 8.2), то для всех транзакций на уровне MS SQL Server принудительно устанавливается максимальный уровень изоляции транзакций — SERIALIZABLE.

Этот уровень изоляции призван полностью защитить транзакцию от эффекта «фантомного чтения» (когда в процессе транзакции параллельный сеанс может вставить новые записи, которые попали бы под условия нашей выборки). Чтобы гарантировать эту защиту, СУБД использует специальный механизм — диапазонные блокировки (Key-Range Locks).

Рассмотрим, что происходит «под капотом» СУБД, когда мы пишем код с использованием объекта РегистрСведенийМенеджерЗаписи:


НачатьТранзакцию();
РегистрМенеджер = РегистрыСведений.КакойНибудьРегистр.СоздатьМенеджерЗаписи();
РегистрМенеджер.Дата = ТекДата; 
РегистрМенеджер.Товар = Товар; 
РегистрМенеджер.Магазин = Магазин; 
РегистрМенеджер.Записать(Истина); // Замещение = Истина
ЗафиксироватьТранзакцию();

При вызове метода Записать(Истина) платформа 1С должна гарантировать уникальность записей в регистре. СУБД не может просто выполнить инструкцию INSERT, так как запись с такими измерениями уже может существовать. Поэтому трансляция этого действия в SQL выглядит следующим образом:

  1. СУБД выполняет команду DELETE по указанному набору измерений, чтобы удалить старую запись (если она была).
  2. СУБД выполняет команду INSERT для записи новых данных.

Поскольку мы работаем на уровне изоляции SERIALIZABLE, при выполнении команды DELETE СУБД пытается наложить блокировку. Но что блокировать, если записи с такими измерениями в таблице еще нет? СУБД не может заблокировать несуществующую физическую строку. В этот момент и включается механизм Key-Range Locks.

Механика диапазонных блокировок (Key-Range Locks) и возникновение взаимоблокировок

Когда СУБД пытается удалить или прочитать несуществующую запись в транзакции уровня SERIALIZABLE, она накладывает блокировку на интервал индекса, в который попадает (или должно было попасть) наше значение. Этот интервал простирается от ближайшего существующего ключа индекса «слева» (меньшего значения) до ближайшего существующего ключа «справа» (большего значения).

На этот диапазон накладывается блокировка типа RangeS-U (Shared range, update resource) — это блокировка чтения диапазона с намерением последующего обновления. Особенность этой блокировки в том, что она совместима с другими аналогичными блокировками RangeS-U на этот же диапазон от других транзакций.

Проанализируем по шагам, как это приводит к взаимоблокировке (Deadlock) или таймауту при попытке двух сеансов записать разные товары:

  1. Сеанс №1 начинает транзакцию и пытается записать «Товар А». СУБД выполняет DELETE, записи нет. Накладывается блокировка RangeS-U на диапазон индекса, включающий «Товар А».
  2. Сеанс №2 параллельно пытается записать «Товар Б». Выполняется DELETE, записи нет. Поскольку «Товар Б» находится в том же или соседнем диапазоне (особенно если регистр пустой или в нем мало записей, из-за чего диапазон расширяется до бесконечности), СУБД успешно накладывает блокировку RangeS-U для Сеанса №2. Обе транзакции сейчас удерживают совместимые блокировки чтения на один и тот же интервал.
  3. Сеанс №1 переходит к шагу INSERT. Чтобы вставить физическую строку в заблокированный диапазон, СУБД требуется повысить (конвертировать) свою блокировку диапазона до монопольной — RangeI-N (Insert range lock). Однако СУБД не может выдать Сеансу №1 монопольную блокировку, так как на этом же диапазоне висит действующая блокировка RangeS-U от Сеанса №2. Сеанс №1 встает в очередь ожидания.
  4. Сеанс №2 также переходит к шагу INSERT и пытается конвертировать свою блокировку в RangeI-N. Но ему мешает блокировка RangeS-U, удерживаемая Сеансом №1.
  5. Возникает классическая тупиковая ситуация (Deadlock). СУБД MS SQL Server через некоторое время распознает эту взаимоблокировку и принудительно завершает одну из транзакций аварийно, либо один из клиентов отваливается по таймауту ожидания блокировки 1С.

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

Решение проблемы

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

Вариант 1. Переход на управляемый режим блокировок (Рекомендуемый)

Это единственно верный и современный архитектурный подход. Перевод регистра в управляемый режим кардинально меняет поведение СУБД.

Выполним следующие шаги:

  1. В свойствах конфигурации или конкретного регистра сведений установим свойство «Режим управления блокировками» в значение «Управляемый».
  2. На стороне СУБД MS SQL Server обязательно включим режим версионирования строк — RCSI (Read Committed Snapshot Isolation).

Что меняется при этом на уровне базы данных?

При использовании управляемого режима платформа 1С переводит транзакции в СУБД на уровень изоляции READ COMMITTED. На этом уровне диапазонные блокировки (Key-Range Locks) СУБД больше вообще не используются!

При записи данных MS SQL Server накладывает блокировки только на конкретные существующие физические строки (блокировки типа RID или KEY), а операции вставки (INSERT) не требуют предварительного захвата диапазона. Контроль логической целостности данных переносится на уровень самой платформы 1С с помощью механизма управляемых блокировок. Поскольку измерения у наших транзакций не пересекаются, менеджер блокировок 1С считает их полностью совместимыми. Запись в СУБД выполняется абсолютно параллельно и без задержек.

Вариант 2. Запись без замещения через НаборЗаписей (Обходной путь)

Если ваша конфигурация слишком старая, работает под сильной нагрузкой в автоматическом режиме блокировок и у вас нет возможности прямо сейчас перевести ее на управляемый режим, можно обойти проблему диапазонных блокировок. Наша задача — заставить СУБД выполнять чистый INSERT, минуя фазу DELETE.

Для этого мы откажемся от использования МенеджерЗаписи и перепишем код на работу с объектом НаборЗаписей:


НачатьТранзакцию();
ТекДата = ТекущаяДата();

Набор = РегистрыСведений.КакойНибудьРегистр.СоздатьНаборЗаписей();
Набор.Отбор.Дата.Установить(ТекДата);
Набор.Отбор.Товар.Установить(Товар);
Набор.Отбор.Магазин.Установить(Магазин);

НоваяЗапись = Набор.Добавить();
НоваяЗапись.Дата = ТекДата;
НоваяЗапись.Товар = Товар;
НоваяЗапись.Магазин = Магазин;

// Включаем режим загрузки, чтобы отключить лишние платформенные проверки
Набор.ОбменДанными.Загрузка = Истина;

// Самый важный параметр: Замещение = Ложь
Набор.Записать(Ложь); 

ЗафиксироватьТранзакцию();

Разберем по шагам, почему этот код решает проблему блокировок:

Когда мы передаем в метод Записать() параметр Ложь, мы сообщаем платформе, что старые записи в регистре заменять не нужно. Платформа 1С оптимизирует запрос к СУБД и полностью исключает предварительный вызов команды DELETE. В базу данных отправляется прямой одиночный запрос INSERT.

Поскольку операции DELETE нет, СУБД не накладывает диапазонную блокировку RangeS-U. Прямая вставка новой записи не конфликтует с параллельной вставкой другого товара в соседний диапазон. Параллельность работы значительно повышается.

Важное предупреждение при использовании этого обходного пути:

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

Резюме

Для обеспечения стабильной и высокопроизводительной параллельной работы в 1С всегда стремитесь использовать управляемый режим блокировок. Это современный стандарт разработки, который решает проблемы блокировок на уровне архитектуры платформы. Использование же обходных путей с записью без замещения допустимо лишь как временная мера в унаследованных (legacy) системах.

← На главную