Как установить управляемую блокировку по произвольному полю документа в 1С для предотвращения дублей

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

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

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

Причина кроется в особенностях работы транзакций СУБД и уровнях изоляции данных. В режиме управляемых блокировок платформа 1С настраивает СУБД на уровень изоляции Read Committed (или его аналог RCSI в СУБД MS SQL). Этот уровень изоляции гарантирует, что транзакция может считывать только зафиксированные (committed) данные.

Когда два параллельных потока одновременно пытаются записать документ с одним и тем же входящим номером, происходит следующая последовательность событий:

  1. Поток №1 открывает транзакцию и выполняет проверочный запрос к СУБД. База данных отвечает: «Документа с входящим номером УТ-0000140 не существует». Поток №1 начинает формировать и записывать документ, но транзакция еще не завершена.
  2. Поток №2 параллельно открывает транзакцию и делает точно такой же проверочный запрос. Поскольку транзакция Потока №1 еще не зафиксирована (не сделан коммит), Поток №2 физически не может увидеть новые данные Потока №1 в силу ограничений уровня изоляции Read Committed. База данных снова отвечает: «Документа с входящим номером УТ-0000140 не существует».
  3. Оба потока успешно записывают свои документы. В результате в системе появляются два абсолютно одинаковых документа, что приводит к дальнейшим ошибкам в учете и формировании движений по регистрам. Это классическое «состояние гонки» (race condition).

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

Способ 1. Использование штатного свойства метаданных «Поля блокировки данных»

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

Чтобы решить эту проблему в рамках основной конфигурации, выполним следующие шаги:

  1. Откроем конфигуратор и найдем нужный объект метаданных (в нашем примере это документ СчетФактураПолученный).
  2. Перейдем на вкладку Прочее в окне редактирования объекта конфигурации.
  3. Найдем свойство Поля блокировки данных и откроем его.
  4. В появившемся списке выберем реквизит, по которому планируется выполнять блокировку — НомерВходящегоДокумента.

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


НачатьТранзакцию();

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

Попытка
    // Накладываем управляемую блокировку на конкретное значение входящего номера
    Блокировка.Заблокировать();
Исключение
    ОтменитьТранзакцию();
    ЗаписьЖурналаРегистрации("ЗагрузкаДанных", УровеньЖурналаРегистрации.Ошибка, , , 
        "Не удалось заблокировать входящий номер: " + ОписаниеОшибки());
    ВызватьИсключение "Превышено время ожидания блокировки. Пожалуйста, повторите попытку позже.";
КонецПопытки;

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

Запрос.УстановитьПараметр("НомерВходящегоДокумента", Источник.НомерВходящегоДокумента);
Запрос.УстановитьПараметр("ТекущаяСсылка", Источник.Ссылка);

РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
    ОтменитьТранзакцию();
    ВызватьИсключение "Документ с входящим номером " + Источник.НомерВходящегоДокумента + " уже зарегистрирован в базе данных!";
КонецЕсли;

// Если проверка пройдена, выполняем запись документа и фиксируем транзакцию
ЗафиксироватьТранзакцию();

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

Способ 2. Паттерн «Виртуальный регистр блокировок» (Решение для расширений)

Для реализации механизма блокировок в расширении без изменения типовых метаданных мы применим профессиональный паттерн под названием «Виртуальный регистр-семафор». Менеджер управляемых блокировок платформы 1С работает на уровне собственного сервера приложений и оперирует виртуальными координатами объектов метаданных. Ему не важно, записаны ли физические данные в СУБД. Блокируя виртуальный ресурс по уникальному строковому значению, мы запрещаем другим транзакциям захватывать этот же ресурс.

Разберем по шагам, как реализовать этот паттерн:

  1. Создадим в нашем расширении конфигурации новый независимый непериодический Регистр сведений (назовем его, например, БлокировкаВходящихНомеров).
  2. Создадим в нем единственное измерение — НомерВходящегоДокумента с типом Строка (длина 50–100 символов в зависимости от потребностей).
  3. Важно: Физически делать записи в данный регистр сведений мы не будем никогда! Он будет выполнять исключительно роль виртуальной координатной сетки для менеджера блокировок.
  4. Добавим логику блокировки и проверки перед записью в расширение документа.

Посмотрим на пример кода, который мы разместим в обработчике события ПередЗаписью расширения нашего документа:


&Перед("ПередЗаписью")
Процедура Расш1_ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
    
    // Блокировка и проверки должны выполняться только внутри транзакции записи
    Если Отказ Или ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;
    
    // Нам нужен только заполненный входящий номер
    Если ПустаяСтрока(ЭтотОбъект.НомерВходящегоДокумента) Тогда
        Возврат;
    КонецЕсли;
    
    // Инициализируем управляемую блокировку по виртуальному регистру
    Блокировка = Новый БлокировкаДанных;
    ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.БлокировкаВходящихНомеров");
    ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
    
    // Блокируем конкретную строковую координату входящего номера
    ЭлементБлокировки.УстановитьЗначение("НомерВходящегоДокумента", ЭтотОбъект.НомерВходящегоДокумента);
    
    // Вызов метода Заблокировать() приостановит выполнение текущего потока, 
    // если другой поток прямо сейчас обрабатывает этот же номер входящего документа
    Блокировка.Заблокировать();
    
    // Теперь, когда мы единственные владельцы блокировки на этот номер, делаем запрос к СУБД
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ ПЕРВЫЕ 1
        |	СчетФактураПолученный.Ссылка КАК Ссылка
        |ИЗ
        |	Документ.СчетФактураПолученный КАК СчетФактураПолученный
        |ГДЕ
        |	СчетФактураПолученный.НомерВходящегоДокумента = &НомерВходящегоДокумента
        |	И НЕ СчетФактураПолученный.Ссылка = &ТекущаяСсылка";
        
    Запрос.УстановитьПараметр("НомерВходящегоДокумента", ЭтотОбъект.НомерВходящегоДокумента);
    Запрос.УстановитьПараметр("ТекущаяСсылка", ЭтотОбъект.Ссылка);
    
    РезультатЗапроса = Запрос.Выполнить();
    Если Не РезультатЗапроса.Пустой() Тогда
        Отказ = Истина;
        ВызыватьИсключение "Внимание! Обнаружена попытка создания дублирующего документа! " 
            + "Документ с входящим номером " + ЭтотОбъект.НомерВходящегоДокумента + " уже сохранен в системе.";
    КонецЕсли;

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

Этот элегантный и лаконичный метод гарантирует стопроцентную защиту от дублирования данных. Любой параллельный поток, пытающийся записать аналогичный документ, приостановит свою работу на строке Блокировка.Заблокировать() и будет послушно ожидать завершения транзакции первого потока. Как только первый поток запишет документ и завершит транзакцию, второй поток продолжит работу, выполнит проверочный запрос, обнаружит уже созданный документ и безопасно прервет выполнение с генерацией исключения.

Критика альтернативных вариантов решения

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

1. Использование метода ЗаблокироватьДанныеДляРедактирования()

Данный метод реализует механизм пессимистической объектной блокировки. Проблема заключается в том, что он жестко завязан на конкретную ссылку уже существующего в базе данных объекта. При создании нового документа в базе данных его ссылка фактически является пустой (значение ДокументСсылка.СчетФактураПолученный.ПустаяСсылка()). Если вы попытаетесь заблокировать пустую ссылку, вы заблокируете возможность создания абсолютно всех новых документов данного типа для всех пользователей в системе, что мгновенно парализует работу предприятия.

2. Использование вспомогательного справочника-семафора

Иногда разработчики предлагают перед записью документа создавать элемент специального справочника (например, Справочник.ВходящиеНомераКандидаты), записывать его в СУБД, блокировать его ссылку пессимистическим методом, а по окончании транзакции удалять его. Этот метод рабочий, но он обладает серьезным недостатком: постоянное создание и последующее физическое удаление записей в СУБД ведет к сильной фрагментации индексов таблиц СУБД и разрастанию логов транзакций СУБД (особенно критично для баз данных на PostgreSQL и MS SQL под высокой нагрузкой).

Резюме и рекомендации

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

← На главную