В процессе разработки высоконагруженных систем на платформе 1С:Предприятие часто возникает ситуация, когда несколько пользователей одновременно пытаются записать данные в один и тот же РегистрСведений. Если при этом программный код рассчитывает некое уникальное значение (например, номер или идентификатор по принципу "Максимальный + 1"), возникают конфликты блокировок. Вместо ожидаемого ожидания своей очереди, пользователи получают сообщение об ошибке. В этой статье мы подробно разберем, как правильно настроить механизм блокировок, чтобы система выстраивала обращения в очередь и корректно обрабатывала транзакции.
Первым делом проанализируем типичную ошибку в коде. Часто разработчики пытаются установить блокировку до начала транзакции. В режиме управляемых блокировок вызов Блокировка.Заблокировать() вне транзакции либо не имеет смысла, либо работает некорректно. Кроме того, если система мгновенно выдает ошибку, это может свидетельствовать о возникновении Deadlock (взаимоблокировки). Это ситуация, когда два процесса заблокировали ресурсы и бесконечно ждут друг друга. Система 1С или СУБД обнаруживает этот цикл и принудительно прерывает одну из транзакций.
Рассмотрим также стандартное время ожидания. По умолчанию платформа ждет освобождения ресурса 20 секунд. Если ошибка "Объект заблокирован" появляется раньше или сразу, значит, порядок обращения к объектам в коде нарушен. Наша задача — сделать блокировку максимально короткой и точечной.
Разберем правильный паттерн работы с объектом БлокировкаДанных. Блокировка должна устанавливаться строго внутри транзакции. Если мы сначала вызываем блокировку, а потом НачатьТранзакцию(), мы нарушаем логику работы менеджера блокировок.
Правильный алгоритм действий выглядит следующим образом:
Исключительный, чтобы никто другой не мог начать чтение или запись в этот момент.Заблокировать().Посмотрим на пример правильного кода:
НачатьТранзакцию();
Попытка
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.ДопСведения");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
// Блокируем весь регистр или конкретный диапазон
Блокировка.Заблокировать();
// Теперь мы в безопасности, другие сеансы будут ждать в очереди здесь
Запрос = Новый Запрос("ВЫБРАТЬ МАКСИМУМ(Номер) ИЗ РегистрСведений.ДопСведения");
Результат = Запрос.Выполнить();
// ... логика получения нового номера и записи ...
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации("Ошибка записи", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
ВызватьИсключение "Не удалось записать данные из-за высокой нагрузки. Попробуйте еще раз.";
КонецПопытки;
Блокировка всего регистра — это радикальный метод, который может замедлить работу всей системы ("антипаттерн"). Проанализируем более изящный способ: блокировку вспомогательного объекта. Вместо того чтобы закрывать доступ ко всему регистру ДопСведения, мы можем блокировать одну конкретную запись в специальном маленьком регистре (назовем его Счетчики) или использовать Константу.
Выясним, как это работает: все пользователи при попытке получить номер обращаются к одной и той же записи в маленькой таблице. Платформа выстраивает их в очередь именно на эту запись. Пока один пользователь не закончит работу, остальные ждут. При этом основной регистр остается доступным для чтения другими отчетами и процессами.
Рассмотрим пример с константой:
НачатьТранзакцию();
// Блокируем константу, которая служит "ключом" для очереди
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Константа.ПоследнийИД");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
// Получаем значение, увеличиваем и записываем обратно
НовыйНомер = Константы.ПоследнийИД.Получить() + 1;
Константы.ПоследнийИД.Установить(НовыйНомер);
// Используем полученный номер для записи в основной регистр
// ... логика записи ...
ЗафиксироватьТранзакцию();
Проанализируем ситуацию, когда за один раз нужно добавить сотни записей. Если делать расчет Max + 1 внутри цикла для каждой строки, время транзакции увеличивается в геометрической прогрессии, что неизбежно ведет к ошибкам блокировки у других пользователей.
Рекомендуется использовать следующую стратегию:
НаборЗаписей.Записать().Это сокращает время удержания блокировки с нескольких секунд до миллисекунд.
Если бизнес-логика допускает небольшое ожидание, мы можем реализовать собственную очередь с помощью цикла. Это полезно, когда стандартных 20 секунд ожидания платформы может не хватить из-за очень тяжелых операций.
Посмотрим, как реализовать цикл повторений:
УспешноЗаписано = Ложь;
КоличествоПопыток = 0;
Пока Не УспешноЗаписано И КоличествоПопыток < 5 Цикл
НачатьТранзакцию();
Попытка
// Установка блокировки и логика записи
// ...
ЗафиксироватьТранзакцию();
УспешноЗаписано = Истина;
Исключение
ОтменитьТранзакцию();
КоличествоПопыток = КоличествоПопыток + 1;
// Пауза перед следующей попыткой (имитация через задержку)
// Для серверного кода можно использовать небольшую паузу
КонецПопытки;
КонецЦикла;
Если Не УспешноЗаписано Тогда
ВызватьИсключение "Превышено количество попыток доступа к регистру.";
КонецЕсли;
Выясним, является ли РегистрСведений оптимальным местом для хранения данных с уникальными номерами. Если ваша задача — просто хранить записи с уникальным кодом, рассмотрите использование Справочника. У справочников механизм генерации кодов оптимизирован на уровне платформы. При включенном свойстве Автонумерация и режиме Освобождать автоматически, 1С получает новый номер крайне быстро, используя внутренние кэшированные последовательности СУБД, что практически исключает конфликты при генерации ID.
Подведем итог: для создания очереди необходимо перенести вызов Блокировка.Заблокировать() внутрь транзакции, стремиться к максимальному сокращению времени между чтением номера и записью, и, по возможности, использовать вспомогательные объекты для управления очередью доступа.