При разработке механизмов пакетного ввода документов (например, массового создания Приходных кассовых ордеров на основании Расходных накладных) разработчики часто сталкиваются с проблемой конфликтов блокировок. В многопользовательской среде, когда несколько операторов одновременно запускают процесс генерации документов, система начинает выдавать ошибку: «Конфликт блокировок при выполнении транзакции. Превышено максимальное время ожидания предоставления блокировки».
В этой статье мы подробно разберем, почему возникают такие конфликты, проанализируем распространенные ошибки в коде и рассмотрим несколько эффективных сценариев оптимизации — от правильного написания запросов до изменения архитектуры проведения документов с использованием очередей.
Для начала выясним первопричину проблемы. Когда мы открываем транзакцию и начинаем последовательно создавать, заполнять и проводить документы в цикле, платформа 1С:Предприятие накладывает блокировки на таблицы базы данных (как управляемые блокировки 1С, так и транзакционные блокировки СУБД). Чем дольше длится транзакция, тем выше вероятность того, что другой сеанс попытается обратиться к тем же ресурсам и встанет в очередь ожидания. Если время ожидания превышает установленный лимит (по умолчанию 20 секунд), второй пользователь получает системную ошибку.
Основными факторами, затягивающими транзакцию и вызывающими конфликты, являются:
Рассмотрим по шагам, как исправить каждую из этих проблем.
Проанализируем типичный ошибочный код, в котором данные для заполнения ПКО извлекаются непосредственно из строки табличной части и связанных документов:
// ПЛОХОЙ КОД: Обращения через точку в цикле транзакции
НачатьТранзакцию();
Для Каждого Строка Из Объект.РасходныеНакладные Цикл
НовыйДокумент = Документы.ПриходныйКассовыйОрдер.СоздатьДокумент();
НовыйДокумент.Организация = Строка.РН.Организация; // Обращение через точку!
НовыйДокумент.Контрагент = Строка.РН.Контрагент; // Обращение через точку!
// ... заполнение остальных реквизитов
НовыйДокумент.Записать(РежимЗаписиДокумента.Проведение);
КонецЦикла;
ЗафиксироватьТранзакцию();
При каждом обращении к конструкции Строка.РН.Организация платформа 1С неявно считывает из СУБД весь объект РасходнаяНакладная и кэширует его. В цикле по сотне накладных это порождает сотни лишних запросов к базе данных прямо внутри транзакции. Транзакция растягивается по времени, блокируя таблицы СУБД.
Как сделать правильно: Все необходимые данные нужно получить одним запросом до начала транзакции. Посмотрим на пример оптимизированного кода:
// ХОРОШИЙ КОД: Предварительная выборка данных запросом
МассивРН = Новый Массив;
Для Каждого Строка Из Объект.РасходныеНакладные Цикл
Если ЗначениеЗаполнено(Строка.РН) Тогда
МассивРН.Добавить(Строка.РН);
КонецЕсли;
КонецЦикла;
// Получаем данные одним запросом вне транзакции
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Ссылка КАК РН,
| Организация КАК Организация,
| Контрагент КАК Контрагент,
| Партнер КАК Партнер,
| Договор КАК Договор
|ИЗ
| Документ.РеализацияТоваровУслуг
|ГДЕ
| Ссылка В(&МассивРН)";
Запрос.УстановитьПараметр("МассивРН", МассивРН);
ТаблицаДанных = Запрос.Выполнить().Выгрузить();
// Теперь открываем транзакцию и быстро создаем документы по готовой таблице
НачатьТранзакцию();
Попытка
Для Каждого СтрокаДанных Из ТаблицаДанных Цикл
НовыйДокумент = Документы.ПриходныйКассовыйОрдер.СоздатьДокумент();
НовыйДокумент.Дата = ТекущаяДатаСеанса();
НовыйДокумент.Организация = СтрокаДанных.Организация;
НовыйДокумент.Контрагент = СтрокаДанных.Контрагент;
// Заполнение табличной части и проведение
НовыйДокумент.Записать(РежимЗаписиДокумента.Проведение);
КонецЦикла;
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение; // Передаем ошибку выше для корректной обработки
КонецПопытки;
Этот простой шаг сокращает время выполнения транзакции в десятки раз, минимизируя время удержания блокировок.
Если два пользователя одновременно запускают создание ПКО по разным спискам накладных, содержащих одинаковые элементы, они могут заблокировать ресурсы крест-накрест. Например, Первый пользователь заблокировал Накладную №1 и ждет Накладную №2, а Второй пользователь заблокировал Накладную №2 и ожидает Накладную №1. Происходит взаимная блокировка.
Чтобы этого избежать, применим два правила:
БлокировкаДанных, чтобы заблокировать целевые объекты в самом начале транзакции, не дожидаясь автоматических блокировок при записи.Разберем пример реализации наложения управляемой блокировки с предварительной сортировкой:
// Сортируем массив ссылок перед установкой блокировок
ТаблицаДляСортировки = Новый ТаблицаЗначений;
ТаблицаДляСортировки.Колонки.Добавить("Ссылка");
Для Каждого Ссылка Из МассивРН Цикл
ТаблицаДляСортировки.Добавить().Ссылка = Ссылка;
КонецЦикла;
ТаблицаДляСортировки.Сортировать("Ссылка Возр");
ОтсортированныйМассивРН = ТаблицаДляСортировки.ВыгрузитьКолонку("Ссылка");
// Устанавливаем управляемую блокировку
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Документ.РеализацияТоваровУслуг");
ЭлементБлокировки.ИсточникДанных = ОтсортированныйМассивРН;
ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Ссылка", "Ссылка");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
НачатьТранзакцию();
Попытка
Блокировка.Заблокировать(); // Блокируем сразу все накладные пакетом
// Выполняем создание и проведение ПКО...
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ВызватьИсключение;
КонецПопытки;
В коде из сообщений форума часто встречается попытка обернуть запись каждого документа в конструкцию Попытка — Исключение внутри одной большой транзакции, чтобы «пропустить» проблемные документы и продолжить проведение остальных. Разберем, почему так делать нельзя.
Если внутри транзакции происходит системная ошибка базы данных (например, конфликт блокировок СУБД или ошибка уникальности), транзакция на уровне SQL-сервера немедленно переходит в состояние Rollback Only (в терминологии 1С — транзакция становится «битой»). Любая последующая попытка записать данные, прочитать константу или выполнить запрос в рамках этой транзакции вызовет новое исключение: «В данной транзакции уже происходили ошибки!».
Если вам требуется логика «проводить то, что проводится, а ошибки просто логировать», вам необходимо полностью отказаться от единой транзакции и выполнять транзакции изолированно для каждого документа:
// Проведение каждого документа в индивидуальной транзакции
Для Каждого СтрокаДанных Из ТаблицаДанных Цикл
НачатьТранзакцию();
Попытка
НовыйДокумент = Документы.ПриходныйКассовыйОрдер.СоздатьДокумент();
// Заполнение данных...
НовыйДокумент.Записать(РежимЗаписиДокумента.Проведение);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию(); // Отменяем транзакцию только по текущему документу
ЗаписатьЛогОшибки(СтрокаДанных.РН, ОписаниеОшибки()); // Фиксируем ошибку
КонецПопытки;
КонецЦикла;
Если бизнес-процесс позволяет отказаться от моментального проведения документов «здесь и сейчас», лучшим решением проблемы блокировок станет использование очереди отложенного проведения. Это переносит ресурсоемкую операцию проведения в фоновый режим, убирая нагрузку с интерактивного сеанса пользователя.
Рассмотрим по шагам схему реализации очереди:
ОчередьОтложенногоПроведения с измерениями ДокументСсылка (Тип: ДокументСсылка.ПриходныйКассовыйОрдер) и ресурсом Состояние (Тип: Перечисление.СостоянияОтложенногоПроведения или просто Число).ОчередьОтложенногоПроведения со статусом «В очереди».Этот подход гарантирует пользователю моментальный отклик интерфейса, а система полностью избавляется от конкуренции за таблицы итогов в режиме реального времени.
Если база данных 1С развернута на MS SQL Server, критически важно проверить режим изоляции транзакций. По умолчанию SQL Server при чтении данных накладывает разделяемые блокировки (Shared Locks), которые конфликтуют с исключительными блокировками на запись.
Включение режима Read Committed Snapshot Isolation (RCSI) позволяет читающим транзакциям не блокировать пишущие, а пишущим — не блокировать читающие, за счет использования версионирования строк в TempDB.
Проверить статус версионирования в MS SQL Server можно следующим запросом:
SELECT name, snapshot_isolation_state_desc, is_read_committed_snapshot_on
FROM sys.databases
WHERE name = 'Имя_Вашей_Базы';
Если параметры отключены, включите их с помощью административного скрипта (выполняется при монопольном доступе к базе данных):
ALTER DATABASE [Имя_Вашей_Базы] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
ALTER DATABASE [Имя_Вашей_Базы] SET ALLOW_SNAPSHOT_ISOLATION ON;
ALTER DATABASE [Имя_Вашей_Базы] SET READ_COMMITTED_SNAPSHOT ON;
ALTER DATABASE [Имя_Вашей_Базы] SET MULTI_USER;
Конфликты блокировок — это всегда следствие неоптимальной работы с базой данных или просчетов в архитектуре. Чтобы навсегда решить эту проблему при пакетном создании документов:
БлокировкаДанных со строгой предварительной сортировкой списков документов.