Управляемая блокировка 1С не работает: шесть причин, по которым замок не ставится
Иван Недомолков · Dərc edilib:
Симптом выглядит так: в модуле проведения стоит БлокировкаДанных, код прошёл ревью, а данные ведут себя как без защиты. Два сеанса записывают один элемент, выживает последняя запись. Остаток уходит в минус при живой проверке достаточности. Появляется двойной резерв на одну ячейку.
Разбор таких историй почти всегда упирается в одно: замок, на который все рассчитывали, не ставился. Метод Заблокировать() о своём неучастии не сообщает. Если блокировка не установлена или не действует, вызывающий код отрабатывает без ошибок и предупреждений - исключение вы получите только при конфликте живого замка, а мёртвый замок не конфликтует ни с кем.
Ниже шесть мест, где блокировка теряется молча. Порядок - от дешёвых проверок к дорогим. Список собран на ревизии складской системы крупной розничной сети: из 95 мест, где по смыслу операции нужен замок, 41 опиралось на процедуры, которые выглядят как блокировка, но замка не ставят.
Причина 1: объект работает в автоматическом режиме блокировок
У конфигурации есть свойство “Режим управления блокировкой данных”, и у каждого объекта метаданных - своё такое же. В смешанном режиме (“Автоматический и управляемый”) часть объектов остаётся в автоматическом, и транзакции по ним платформа ведёт автоматически: сериализацию обеспечивают замки СУБД через уровень изоляции, а расставленные кодом управляемые замки в такой транзакции ничего не сериализуют.
Вызов при этом проходит без ошибок. Код написан, ревью довольно, защиты нет.
Проверка занимает минуту: откройте свойства объекта в конфигураторе и посмотрите режим. В той ревизии, с которой начался этот текст, в автоматическом режиме оставалось 27 объектов из 405 - и исключили их из проверки с формулировкой “там только журналы и инфраструктура”, которую никто не перепроверял. Если хоть один такой объект участвует в движении товара, картина блокировок по нему другая, чем предполагает код.
Причина 2: захват спрятан за флагом, который никто не включает
Самый злой случай, потому что процедура называется правильно и внутри выглядит правильно:
Процедура ЗаблокироватьОстатки(Номенклатура, Ячейка, УстанавливатьБлокировку = Ложь)
Блокировка = Новый БлокировкаДанных;
Элемент = Блокировка.Добавить("РегистрНакопления.ОстаткиТоваров");
Элемент.УстановитьЗначение("Номенклатура", Номенклатура);
Элемент.УстановитьЗначение("Ячейка", Ячейка);
Элемент.Режим = РежимБлокировкиДанных.Исключительный;
Если УстанавливатьБлокировку Тогда
Блокировка.Заблокировать();
КонецЕсли;
КонецПроцедуры
Объект блокировки собирается целиком, режим исключительный, всё на месте. Захват стоит под условием, умолчание у флага - Ложь, и все старые вызовы проходят мимо Заблокировать().
Появляется такой флаг из осторожности: кто-то добавил параметр, включил его в одном новом месте и не решился трогать существующие вызовы. Через год про флаг никто не помнит, а имя процедуры продолжает обещать замок. Человек, читающий тело сверху вниз, видит Исключительный и до условия долистывает не всегда.
Отдельно про смену умолчания. Если у такой процедуры позже меняют значение флага по умолчанию, поведение всех вызовов меняется без единого символа диффа в местах вызова. Ревью видит одну строку и пропускает правку, которая трогает всю систему. Умолчание у параметра блокировки ставится в строгую сторону, а вызовы чинятся явно, даже когда их сто.
Причина 3: обёртка сама открывает и сама фиксирует транзакцию
Управляемый замок живёт до конца транзакции, в которой установлен. Если процедура-обёртка при вызове вне транзакции открывает собственную, берёт замок и тут же фиксирует - замок снимается раньше, чем вызывающий код успевает прочитать хоть одну запись. Формально блокировка была. Практически её не существовало ни одного полезного такта.
Проверяется чтением тела: ищите НачатьТранзакцию() и ЗафиксироватьТранзакцию() внутри самой обёртки. Если они там есть, посмотрите, что происходит между захватом и фиксацией. Если ничего - это пустышка.
Причина 4: разделяемый режим там, где защищаются от второго писателя
Разделяемый замок не пускает писателей к читателю. Двух писателей между собой он не сериализует: две параллельные записи одного элемента спокойно проходят обе, и от потерянного обновления такой замок не защищает вовсе.
Правило короткое: перед записью берётся исключительный режим. Разделяемый нужен, когда вы читаете данные и хотите, чтобы их не поменяли под вами, - и не годится, когда меняете сами.
Причина 5: проверка стоит выше границы транзакции
Проверка достаточности написана правильно, читает актуальные данные и выполняется корректно. Стоит она на несколько строк выше НачатьТранзакцию() - и этого хватает, чтобы защита не работала.
Два потока приходят к проверке одновременно, оба видят остаток 1, оба решают, что списание допустимо, оба списывают. Замок внутри транзакции этому не мешает: к моменту его установки решение уже принято снаружи. На разобранной системе расстояние в шесть строк между проверкой и границей транзакции стоило трёх классов отказов сразу - отрицательного остатка, двойного резерва и продажи сверх наличия.
Этот шаблон не ловится ни линтером, ни статическим анализом: синтаксис в порядке, вызовы на месте. Находится он только чтением по цепочке вызовов с вопросом “где здесь начинается транзакция”. Возьмите главную проверку достаточности своей предметной области - остаток, лимит, свободное место - и посмотрите, с какой стороны от НачатьТранзакцию() она стоит.
Причина 6: сериализацию делало блокирующее чтение, и его выключили
Пока на MS SQL Server выключен параметр READ_COMMITTED_SNAPSHOT, читающая транзакция берёт на строках разделяемые замки и стоит в очереди к пишущим. Побочный эффект: код вида “прочитал остатки, подумал, записал” выстраивается в очередь сам собой, даже когда управляемого замка в нём нет. Сериализация есть, замка в коде нет.
Включение снимка на чтение (в 1С этому соответствует режим управляемых блокировок с версионником на стороне СУБД) убирает очередь читателей одномоментно и во всех таких местах сразу. Каждое из них превращается в гонку - не через неделю деградации, а в момент переключения.
По дедлокам переключение при этом помогает заметно: на той же системе пик взаимоблокировок упал с 73 в час до 7-8, ночная сумма - примерно с 200 до 20-27. Две метрики дали один порядок улучшения, и обе настоящие. Опасность в том, что видимая часть - минус девяносто процентов дедлоков - приезжает сразу, а тихие гонки в местах с фантомными замками всплывают неделями. Поэтому перед включением снимка на чтение сначала проходят причины 1-5, и только потом переключают. В обратном порядке вы получите улучшение по дедлокам и тихую порчу данных одним действием.
Проверить, как настроен ваш SQL Server и что из настроек меняет поведение блокировок, быстрее готовой обработкой Чек-ап СУБД под 1С, чем собирать запросы по одной.
Проверка на вечер
Порядок, который повторяется на любой чужой конфигурации без подготовки.
- Поиском по конфигурации посчитайте вызовы
Заблокировать(). Отдельно посчитайте процедуры, в именах которых есть “Заблокировать” или “Блокировк”. Если процедур заметно больше, чем захватов, - в конфигурации есть обёртки, и в тело каждой придётся заглянуть. - В каждой обёртке найдите условие над захватом. Параметр с умолчанием Ложь и ветка без захвата - кандидаты в пустышки.
- Сверьте режим замка с назначением: перед записью - исключительный.
- Проверьте, что обёртка не фиксирует транзакцию сама.
- Найдите проверки достаточности и их положение относительно
НачатьТранзакцию(). - Посмотрите список объектов в автоматическом режиме блокировок и решите про каждый, участвует ли он в конкурентных операциях.
Вывод из всей ревизии один: имя процедуры и зелёное ревью не доказывают ничего. Забытый замок находится по отсутствию вызова, поддельный по отсутствию вызова не находится никогда - его находит только чтение тела и границы транзакции.
Что здесь не разобрано
Сознательно за кадром остались два соседних сюжета. Как искать виновника, когда блокировки живые и конфликтуют по-настоящему, - это разбор взаимоблокировок по частотам, там метод счёта вместо разбора одного случая. И отдельный класс, где движения ни при чём, а дерутся таблицы итогов, - дедлок на итогах регистра.
История нагрузки - кто держал базу ночью и чьи сеансы стояли за конфликтами - снимается обработкой Анализ нагрузки кластера 1С: она пишет снимки по расписанию и отвечает именами держателей вместо голых счётчиков.
Если минусовые остатки и потерянные обновления уже стали регулярными, а ревизию блокировок делать некому - напишите нам. Начинаем с экспресс-аудита: за пару дней станет видно, где в вашей конфигурации замки настоящие, а где декорация.