Управляемые блокировки

Разбираем вопрос, на котором сыпется большинство джунов: почему проверки «хватает ли остатка» недостаточно, если рядом её же выполняет второй пользователь.

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

Формулировка вопроса на собеседовании обычно звучит так: «Документ списывает товар. Перед проведением вы проверяете, что остаток на складе не уйдёт в минус. Достаточно ли просто прочитать остаток запросом и сравнить с числом?» Правильный ответ — нет, и умение объяснить почему отделяет джуна от мидла.

Зачем это нужно на практике

Представьте: на складе лежит 1 штука товара. Два менеджера одновременно проводят расходные документы на эту же штуку. Оба читают остаток — оба видят «1» — оба решают, что провести можно, и оба записывают движение. В итоге вы продали одну штуку дважды и ушли в минус. Это классическая гонка (race condition): между «прочитали остаток» и «записали движение» другой сеанс успел вклиниться.

Управляемая блокировка закрывает этот промежуток. Первый сеанс блокирует строку остатка по этому товару и складу — второй сеанс на своей блокировке ждёт, пока первый закончит транзакцию. Когда второй наконец получает доступ, он перечитывает остаток, видит уже «0» и корректно отказывает в проведении.

Как это выглядит в коде

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

Процедура ОбработкаПроведения(Отказ, Режим)

    Блокировка = Новый БлокировкаДанных;
    Элемент = Блокировка.Добавить("РегистрНакопления.ТоварыНаСкладах.Остатки");
    Элемент.Режим = РежимБлокировкиДанных.Исключительный;

    Для Каждого СтрокаТовары Из Товары Цикл
        Элемент.УстановитьЗначение("Номенклатура", СтрокаТовары.Номенклатура);
        Элемент.УстановитьЗначение("Склад", Склад);
    КонецЦикла;

    Блокировка.Заблокировать();

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

    Выборка = Запрос.Выполнить().Выбрать();
    Пока Выборка.Следующий() Цикл
        Если Выборка.Остаток < 0 Тогда
            Сообщить("Не хватает остатка по: " + Выборка.Номенклатура);
            Отказ = Истина;
        КонецЕсли;
    КонецЦикла;

КонецПроцедуры

Здесь важен объект БлокировкаДанных. Метод Добавить задаёт пространство блокировки — обычно это виртуальная таблица регистра (...Остатки). УстановитьЗначение сужает блокировку до конкретных значений измерений (конкретный товар, конкретный склад), чтобы не блокировать весь регистр целиком. Заблокировать() ставит блокировку в базе.

Разделяемая и исключительная

У блокировки есть режим. Разделяемая (РежимБлокировкиДанных.Разделяемый) — «читаю, не мешайте менять». Несколько разделяемых блокировок на одни данные уживаются, но исключительную рядом не пустят. Исключительная (Исключительный) — «я меняю, никого не пущу ни читать под блокировкой, ни писать». Перед проверкой-и-записью остатка берут именно исключительную: вы собираетесь менять данные, и параллельный доступ к ним недопустим.

Как это работает под капотом

С версии 8.1 у 1С два режима блокировок, и это ровно то, что спрашивают дальше.

Автоматические блокировкиУправляемые блокировки
Ставит СУБД сама, на уровне таблиц/страницСтавит разработчик из кода, точечно по значениям
Уровень изоляции высокий (часто SERIALIZABLE)Уровень изоляции ниже (READ COMMITTED)
Блокирует «широко» — легко ловит лишние конфликтыБлокирует «узко» — меньше конфликтов, выше параллельность
Разработчику проще, но система «встаёт» под нагрузкойТребует явных блокировок в коде, зато масштабируется

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

Как отвечать на собеседовании

Скелет сильного ответа: «Просто прочитать остаток мало — между чтением и записью вклинится параллельный сеанс, будет гонка и минусовой остаток. Нужно внутри транзакции поставить управляемую блокировку объектом БлокировкаДанных на строки регистра остатков по нужным номенклатуре и складу в исключительном режиме, и только после блокировки читать остаток и проверять. Тогда второй сеанс подождёт нашу транзакцию и увидит уже актуальные данные». Если попросят про режимы — объясните разницу автоматических и управляемых блокировок и почему управляемый режим понизил изоляцию ради параллельности.

Частые ошибки на собеседовании

  • «Достаточно проверить остаток запросом». Самый частый провал — не увидеть гонку между чтением и записью.
  • Читать данные до блокировки. Если сначала прочитали остаток, а потом заблокировали — вы защитили запись, но проверяли устаревшее значение. Порядок строгий: блокировка → чтение → проверка.
  • Блокировать весь регистр. Забыть УстановитьЗначение и заблокировать всю таблицу остатков — параллельность падает до нуля, все стоят в очереди. Блокируйте точечно по измерениям.
  • Путать блокировку и транзакцию. Блокировка живёт только внутри транзакции и снимается при её завершении. Управляемая блокировка вне транзакции смысла не имеет.
  • Считать, что управляемая блокировка = блокировка СУБД. Управляемые блокировки 1С живут в собственном менеджере блокировок платформы, а не только в СУБД — это отдельный слой, который платформа согласует с базой.

Итоги-шпаргалка

  • Проверка остатка без блокировки = гонка = минус на складе.
  • Порядок неизменен: транзакция → БлокировкаДанных → чтение → проверка → запись.
  • Добавить задаёт таблицу, УстановитьЗначение сужает по измерениям, Заблокировать ставит блокировку.
  • Режимы: Разделяемый — для чтения, Исключительный — перед изменением.
  • Автоматический режим защищал сам, но «широко» и медленно; управляемый — точечно и быстро, но целостность на разработчике.
Проверьте себя
1. Почему перед списанием товара недостаточно прочитать остаток запросом и сравнить его с нужным количеством?
AЗапрос к регистру остатков всегда возвращает устаревшие данные из-за кеша платформы
BМежду чтением остатка и записью движения параллельный сеанс может вклиниться и провести своё движение — возникает гонка
CМетод Выполнить() не гарантирует порядок строк, поэтому сравнение ненадёжно
DВиртуальная таблица Остатки не поддерживает параметр отбора по складу
2. В чём главное отличие управляемого режима блокировок от автоматического?
AВ управляемом режиме блокировки вообще не используются — целостность обеспечивают триггеры СУБД
BАвтоматический режим ставит разработчик из кода, а управляемый — платформа сама
CВ управляемом режиме уровень изоляции ниже, и разработчик обязан ставить блокировки явно объектом БлокировкаДанных; автоматический блокирует шире и делает это сам
DУправляемый режим доступен только в файловой базе, а автоматический — только в клиент-серверной