Как правильно организовать пакетное создание документов и блокировку данных в 1С

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

Создание документов крупными пакетами в среде «1С:Предприятие 8» — классическая задача, с которой сталкивается практически каждый разработчик при интеграции систем, загрузке данных или проведении регламентных групповых обработок. На первый взгляд алгоритм кажется простым: открыть транзакцию, запустить цикл создания объектов, записать их и зафиксировать транзакцию. Однако в многопользовательской среде этот процесс мгновенно обрастает критическими проблемами: от взаимных блокировок (дедлоков) до зависания интерфейса у всех пользователей информационной базы.

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

1. Взаимодействие транзакций и управляемых блокировок

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

Рассмотрим стандартную схему взаимодействия транзакции и блокировки. Когда мы открываем транзакцию с помощью оператора НачатьТранзакцию(), мы создаем изолированное пространство выполнения операций. Блокировка, установленная сразу после открытия транзакции, гарантирует, что другие сеансы не смогут изменить или создать пересекающиеся данные до тех пор, пока транзакция не будет полностью завершена (зафиксирована или отменена).

Давайте посмотрим на базовый каркас этого механизма:


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

При такой схеме другие пользователи увидят созданные документы только в момент успешного выполнения строки ЗафиксироватьТранзакцию(). До этого момента все изменения находятся внутри транзакционной памяти СУБД. Если в процессе возникнет сбой, транзакция откатится, и база данных вернется в исходное состояние, словно пакетной записи и не происходило.

2. Архитектурная ловушка: «Сломанная транзакция» (Doomed Transaction)

Проанализируем распространенную ошибку начинающих разработчиков, которая часто встречается на форумах. Попытка обернуть в блок Попытка — Исключение только оператор фиксации транзакции (как показано во многих неверных примерах) ведет к катастрофическим последствиям. Если ошибка возникнет внутри цикла записи документов, выполнение процедуры прервется, управление перейдет в обработчик ошибок, но сама транзакция останется открытой («зависнет» в памяти). Это приведет к удержанию блокировок базы данных и параличу работы всей компании.

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

Даже если вы попытаетесь перехватить эту ошибку с помощью конструкции Попытка...Исключение внутри цикла, чтобы продолжить запись остальных документов пакета, база данных больше не позволит совершить ни одной модификации. Любая последующая попытка записи или вызова ЗафиксироватьТранзакцию() завершится критической ошибкой: «В данной транзакции уже происходили ошибки!».

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

3. Опасность блокировки всей таблицы документов

Рассмотрим строчку кода: Блокировка.Добавить("Документы.МойДокумент") без явного указания пространства блокировки и фильтров. Что происходит в этот момент под капотом СУБД?

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

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

Посмотрим на пример реализации такого подхода:


НачатьТранзакцию();
Попытка
    // Блокируем не таблицу документов, а конкретный ключ в служебном регистре
    Блокировка = Новый БлокировкаДанных;
    ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.СлужебныйРегистрСинхронизации");
    ЭлементБлокировки.УстановитьЗначение("ИмяПроцесса", "ПакетнаяЗапись_МойДокумент");
    ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
    Блокировка.Заблокировать();
    
    // Теперь мы гарантируем, что две параллельные пакетные обработки не запустятся одновременно.
    // При этом рядовые пользователи могут без ограничений создавать единичные документы "МойДокумент" вручную!
    
    Для Каждого СтрокаДанных Из ТаблицаДанных Цикл
        НовыйДок = Документы.МойДокумент.СоздатьДокумент();
        // Заполнение реквизитов...
        НовыйДок.Записать();
    КонецЦикла;
    
    ЗафиксироватьТранзакцию();
Исключение
    Если ТранзакцияАктивна() Тогда
        ОтменитьТранзакцию();
    КонецЕсли;
    ВызватьИсключение;
КонецПопытки;

4. Проблема «дыр» в автонумерации документов

При пакетном создании документов внутри транзакции вызов метода Записать() заставляет систему генерировать новый уникальный номер документа. Механизм автонумерации платформы запрашивает у СУБД следующий свободный индекс.

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

Для многих сфер бизнеса (особенно в регламентированном учете при выставлении счетов-фактур или актов) пропуски в нумерации строго недопустимы. Если для вашей задачи критично сохранять сплошную последовательную нумерацию, стандартная автонумерация в длинных транзакциях вам не подойдет. В таких случаях программисты используют альтернативный подход: документы записываются без номеров (или с временными номерами), а финальное назначение реальных номеров происходит в самом конце транзакции, непосредственно перед вызовом фиксации, с использованием кастомных потокобезопасных нумераторов.

5. Оптимизация скорости с помощью ОбменДанными.Загрузка

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

Если ваша пакетная обработка выполняет техническую роль (например, перенос данных из другой базы или загрузку файлов от поставщика), крайне рекомендуется отключать стандартную бизнес-логику прикладного решения. Делается это с помощью установки флага ОбменДанными.Загрузка в значение Истина.

Проанализируем пример ускоренной записи объектов в цикле:


Для Каждого СтрокаДанных Из ИсточникДанных Цикл
    ДокОбъект = Документы.МойДокумент.СоздатьДокумент();
    // Заполнение реквизитов объекта
    ДокОбъект.Дата = СтрокаДанных.Дата;
    
    // Включаем режим загрузки для минимизации проверок
    ДокОбъект.ОбменДанными.Загрузка = Истина;
    
    ДокОбъект.Записать();
КонецЦикла;

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

6. Вынос пакетных процессов в фоновые задания

Пакетная обработка большого массива данных на клиенте или в основном серверном вызове приводит к полному «зависанию» веб-клиента или тонкого клиента пользователя. На экране появляется надпись «Программа не отвечает», а случайный обрыв сетевого соединения со стороны клиента прервет транзакцию на середине работы.

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

7. Итоговый эталонный шаблон пакетного создания документов

Объединим все вышеописанные лучшие практики в единый надежный шаблон кода, готовый к промышленной эксплуатации в высоконагруженных системах:


Процедура ВыполнитьПакетноеСозданиеДокументов(ПараметрыЗапуска) Экспорт
    
    // 1. Инициализируем транзакцию
    НачатьТранзакцию(РежимУправленияБлокировкойДанных.Управляемый);
    
    Попытка
        
        // 2. Устанавливаем точечную блокировку для предотвращения параллельного запуска этого же пакета
        Блокировка = Новый БлокировкаДанных;
        ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.СлужебныйРегистрСинхронизации");
        ЭлементБлокировки.УстановитьЗначение("ИмяПроцесса", "ПакетнаяЗапись_" + ПараметрыЗапуска.ИдентификаторПакета);
        ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
        Блокировка.Заблокировать();
        
        // 3. Выполняем предварительное чтение необходимых данных для заполнения документов
        // (Чтение выполняем внутри транзакции, чтобы данные не изменились другими пользователями в процессе)
        
        Для Каждого СтрокаИзИсточника Из ПараметрыЗапуска.ДанныеДляЗаписи Цикл
            
            НовыйДокумент = Документы.МойДокумент.СоздатьДокумент();
            НовыйДокумент.Дата = ТекущаяДатаСеанса();
            
            // Заполнение реквизитов шапки и табличных частей
            НовыйДокумент.Контрагент = СтрокаИзИсточника.Контрагент;
            НовыйДокумент.СуммаДокумента = СтрокаИзИсточника.Сумма;
            
            // Заполняем табличную часть
            НоваяСтрока = НовыйДокумент.Товары.Добавить();
            НоваяСтрока.Номенклатура = СтрокаИзИсточника.Номенклатура;
            НоваяСтрока.Количество = СтрокаИзИсточника.Количество;
            НоваяСтрока.Цена = СтрокаИзИсточника.Цена;
            
            // Отключаем лишние подписки и тяжелые проверки, если это техническая загрузка
            Если ПараметрыЗапуска.РежимТехническойЗагрузки Тогда
                НовыйДокумент.ОбменДанными.Загрузка = Истина;
            КонецЕсли;
            
            // Записываем документ
            НовыйДокумент.Записать(РежимЗаписиДокумента.Запись);
            
        КонецЦикла;
        
        // 4. Фиксируем транзакцию — только теперь документы физически станут доступны другим пользователям
        ЗафиксироватьТранзакцию();
        
    Исключение
        // 5. В случае любой ошибки гарантированно откатываем все изменения
        Если ТранзакцияАктивна() Тогда
            ОтменитьТранзакцию();
        КонецЕсли;
        
        // Запись ошибки в Журнал регистрации для последующего анализа администратором
        ЗаписьЖурналаРегистрации(
            "ПакетноеСозданиеДокументов",
            УровеньЖурналаРегистрации.Ошибка,
            Метаданные.Документы.МойДокумент,
            ,
            ОписаниеОшибки()
        );
        
        // Пробрасываем исключение дальше для вывода пользователю или логирования в фоновом задании
        ВызватьИсключение;
    КонецПопытки;
    
КонецПроцедуры

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

← На главную