Управляемые блокировки на регистре сведений: почему паттерн read-modify-write опасен и как его исправить
Как работают неявные блокировки на регистрах сведений
Режим управляемых блокировок создавался для максимальной параллельности за счёт низких уровней изоляции в СУБД (Read Committed). Это не означает, что платформа снимает с себя ответственность за данные.
Платформа ставит неявные управляемые блокировки на оба типа регистров сведений:
- При чтении набора записей в транзакции — разделяемая блокировка по значениям основного отбора.
- При записи в транзакции — исключительная блокировка на изменяемый набор записей.
Для регистра, подчинённого регистратору, платформа добавляет исключительную блокировку автоматически при проведении документа. Для независимого регистра этого автоматического шага нет — разработчик добавляет его сам. Сам же механизм неявных транзакционных блокировок работает одинаково для обоих типов.
Миф «у независимого регистра нет блокировок» — неверен. Блокировки есть. Проблема в другом: неявной разделяемой блокировки при чтении недостаточно для паттерна чтение→изменение.
Где возникает гонка: два сценария
Типовая задача: независимый регистр «Следующий номер партии», два сеанса конкурируют за запись.
Сценарий 1: чтение вне транзакции. Без НачатьТранзакцию() платформа расценивает чтение как «безответственное» — оно выполняется без оглядки на чужие блокировки. Оба сеанса читают «номер 5», оба пишут «номер 6». Потерянное обновление — без единого сообщения об ошибке.
Сценарий 2: чтение в транзакции без явной исключительной блокировки.
// Сеанс А:
НачатьТранзакцию(); // → читает «5», получает разделяемую блокировку
// Сеанс Б (параллельно):
НачатьТранзакцию(); // → читает «5», получает разделяемую блокировку
// Сеанс А пытается записать «6» → запрашивает исключительную → ждёт Б
// Сеанс Б пытается записать «6» → запрашивает исключительную → ждёт А
// → Взаимоблокировка (deadlock). Платформа выбросит исключение.
В первом сценарии данные молча повреждаются. Во втором — транзакция падает с ошибкой конфликта блокировок. Неявных блокировок в обоих случаях недостаточно.
Объект БлокировкаДанных: стандартный шаблон платформы
Явная исключительная блокировка до чтения — стандартный шаблон платформы для паттерна чтение→изменение. Пока один сеанс держит исключительную блокировку (в клиент-серверном режиме), второй не может ни прочитать с разделяемой блокировкой, ни записать тот же набор записей.
НачатьТранзакцию();
Попытка
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.КурсыВалют");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.УстановитьЗначение("Валюта", ВалютаСсылка);
ЭлементБлокировки.УстановитьЗначение("Период", НачалоДня(ТекущаяДатаСеанса()));
Блокировка.Заблокировать(); // ← только теперь второй сеанс ждёт
МенеджерЗаписи = РегистрыСведений.КурсыВалют.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Валюта = ВалютаСсылка;
МенеджерЗаписи.Период = НачалоДня(ТекущаяДатаСеанса());
МенеджерЗаписи.Прочитать(); // ← читаем уже под исключительной блокировкой
МенеджерЗаписи.Курс = НовыйКурс;
МенеджерЗаписи.Записать();
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
Что делает каждая строка
Блокировка.Добавить("РегистрСведений.КурсыВалют")— пространство блокировок для независимого регистра:РегистрСведений.<Имя>. Имя совпадает с именем объекта метаданных в конфигураторе.РежимБлокировкиДанных.Исключительный— при использовании явных управляемых блокировок для паттерна чтение→изменение только этот режим гарантирует, что другой сеанс не прочитает данные до конца транзакции.УстановитьЗначение("Поле", Значение)— сужаем область блокировки до конкретных измерений. Без этого блокируется вся таблица, что при нескольких сеансах, работающих с разными измерениями, создаёт избыточное ожидание. Допустимые поля: измерения регистра и реквизит Период (для периодических). Нельзя блокировать по строкам неограниченной длины, хранилищам значений и составным типам, включающим их.Блокировка.Заблокировать()— блокировка реально устанавливается только здесь. До вызова объект лишь конфигурируется в памяти.- Разблокировка в рамках одной транзакции — автоматически при
ЗафиксироватьТранзакцию()илиОтменитьТранзакцию(). Явный вызовРазблокировать()не нужен.
Выбор режима: Исключительный или Разделяемый
Разделяемый режим допускает параллельное чтение несколькими сеансами. Он оправдан, когда данные в этой транзакции не будут изменены данным сеансом — например, несколько сеансов читают для вычислений, а записывает отдельный поток. Разделяемые блокировки совместимы друг с другом, но несовместимы с исключительной.
Если же в той же транзакции планируется запись — используйте Исключительный: только он исключает ситуацию, когда два сеанса одновременно держат разделяемые блокировки и затем оба пытаются повысить их до исключительной.
Поглощение и эскалация блокировок
Два поведения платформы, которые важно знать при работе под нагрузкой.
Поглощение. Платформа склеивает пересекающиеся блокировки. Блокировка, в которой указаны значения не всех измерений, поглотит блокировку с большим числом указанных измерений — при условии совпадения значений общих полей. Если вы заблокировали весь «Склад», блокировка отдельной «Номенклатуры» на этом складе поглощается автоматически.
Эскалация. Жёсткое ограничение платформы: если на одно пространство накладывается более 100 000 блокировок, происходит эскалация — система блокирует пространство целиком. Если в этот момент возникает конфликт с уже наложенными чужими блокировками, эскалация отменяется. Практический вывод: не блокируйте в цикле по одной записи при больших объёмах — задавайте диапазон через УстановитьЗначение по общему измерению.
Три ошибки в реальных проектах
1. Блокировка после чтения. Схема «прочитал → заблокировал → записал» не работает: в промежутке между чтением и блокировкой другой сеанс уже мог прочитать то же значение. Правило одно: данные, которые будут перезаписаны, нужно читать уже после вызова Заблокировать().
2. Чтение вне транзакции. «Для чтения транзакция не нужна» — распространённое заблуждение. Вне транзакции чтение расценивается как безответственное и может вернуть промежуточное состояние. НачатьТранзакцию() обязателен до вызова Заблокировать().
3. Разделяемый режим там, где нужен Исключительный. Если сеанс читает с разделяемой блокировкой и затем пытается записать, а второй сеанс тоже держит разделяемую на те же записи — взаимоблокировка. При использовании явных управляемых блокировок платформы для паттерна чтение→изменение — только Исключительный.
Симптом в production: несколько документов подряд получили одинаковый «следующий номер» или одинаковую «следующую партию» — чтение выполнялось вне транзакции. Если документы падают с ошибкой конфликта блокировок при одновременном проведении — чтение было в транзакции, но без явной исключительной блокировки до чтения. Паттерн один, симптомы разные.
Правило для любого регистра сведений
Неявные блокировки платформы защищают от одновременной неконтролируемой записи. Для паттерна чтение→изменение их недостаточно — это справедливо для обоих типов регистров. Разница лишь в том, что для подчинённого при проведении документа платформа добавляет нужные блокировки сама. Для независимого — добавляет разработчик.
Если алгоритм считывает данные и затем изменяет их в той же транзакции — явная исключительная блокировка до чтения обязательна. Это идиоматическое решение платформы, одинаково применимое для обоих типов регистров.
Перейти в каталог решений →