При параллельной работе пользователей в 1С:Предприятии часто возникает необходимость автоматически присваивать документам или справочникам уникальные номера и штрихкоды. Типичная реализация этой задачи — выборка наибольшего текущего значения из регистра сведений через запрос с агрегатной функцией МАКСИМУМ, инкремент полученного числа на единицу и запись нового значения. Однако после обновления СУБД (например, переход на PostgreSQL 9.4 и выше) или смены версии платформы 1С эта схема может дать сбой: в системе начинают регулярно появляться дубли штрихкодов.
Разберем подробнее причины возникновения этой проблемы, проанализируем нюансы работы блокировок СУБД в подписках на события и рассмотрим способы устранения ошибок генерации штрихкодов.
Посмотрим на пример кода, который часто используется в подписках на события ПриЗаписи для генерации штрихкода EAN-13 в независимый регистр сведений:
Функция СформироватьШтрихКодДокумента(Знач ПрефиксВнутреннегоШтрихкода = Неопределено)
Если ПрефиксВнутреннегоШтрихкода = Неопределено Тогда
ПрефиксВнутреннегоШтрихкода = Константы.ПрефиксВнутреннегоШтрихкода.Получить();
КонецЕсли;
ПрефиксВнутреннегоШтрихкода = Формат(ПрефиксВнутреннегоШтрихкода, "ЧЦ=2; ЧН=; ЧВН=");
ПрефиксКодаДокумента = "9";
Запрос = Новый Запрос("
|ВЫБРАТЬ
| МАКСИМУМ(РегШтрихКоды.Штрихкод) КАК Код
|ИЗ
| РегистрСведений.ШтрихКодыДокументов КАК РегШтрихКоды");
Выборка = Запрос.Выполнить().Выбрать();
Выборка.Следующий();
Если Выборка.Код = NULL Тогда
ВыбКод = 1;
Иначе
ВыбКод = Сред(Выборка.Код, 5, 8);
КонецЕсли;
ТекКод = Мин(ОбщегоНазначения.ПривестиСтрокуКЧислу(ВыбКод) + 1, 99999999);
Штрихкод = "2" + ПрефиксКодаДокумента + ПрефиксВнутреннегоШтрихкода + Формат(ТекКод, "ЧЦ=8; ЧВН=; ЧГ=");
Штрихкод = Штрихкод + КонтрольныйСимволEAN(ШтрихКод, 13);
Возврат Штрихкод;
КонецФункции
При записи документа вызывается процедура, которая проверяет наличие штрихкода для текущей ссылки. Если штрихкод отсутствует, открывается транзакция, накладывается управляемая блокировка и создается менеджер записи регистра сведений:
Процедура УстановкаШКДокументаПриЗаписи(Источник, Отказ) Экспорт
Если СписокДокументовШК.НайтиПоЗначению(Источник.Метаданные().Имя) <> Неопределено Тогда
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ШтрихкодыДокументов.ШтрихКод
|ИЗ
| РегистрСведений.ШтрихкодыДокументов КАК ШтрихкодыДокументов
|ГДЕ
| ШтрихкодыДокументов.Документ = &Документ";
Запрос.УстановитьПараметр("Документ", Источник.Ссылка);
Результат = Запрос.Выполнить().Выбрать();
Если Результат.Количество() = 0 Тогда
НачатьТранзакцию(РежимУправленияБлокировкойДанных.Управляемый);
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить();
ЭлементБлокировки.Область = "РегистрСведений.ШтрихкодыДокументов";
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
ЗаписьШтрихКод = РегистрыСведений.ШтрихкодыДокументов.СоздатьМенеджерЗаписи();
ЗаписьШтрихКод.Документ = Источник.Ссылка;
ЗаписьШтрихКод.ТипШтрихКода = ПланыВидовХарактеристик.ТипыШтрихкодов.EAN13;
ЗаписьШтрихКод.ШтрихКод = СформироватьШтрихКодДокумента();
ЗаписьШтрихКод.Записать(Ложь);
ЗафиксироватьТранзакцию();
КонецЕсли;
КонецЕсли;
КонецПроцедуры
На первый взгляд, наложение исключительной блокировки на объект РегистрСведений.ШтрихкодыДокументов должно полностью защитить от одновременного чтения и записи. Однако на практике два параллельных сеанса все равно получают одинаковые значения штрихкода. Выясним основные причины этой нестабильности.
Проанализируем механику работы платформ 1С при записи документа. Процедура подписки на событие ПриЗаписи всегда выполняется внутри неявной транзакции, которую сама платформа открывает при сохранении объекта ДокументОбъект.
В платформе 1С вложенные транзакции являются условными — вызов НачатьТранзакцию() не открывает новую изолированную транзакцию в СУБД, а лишь увеличивает внутренний счетчик транзакций 1С. При этом режим управления блокировками определяется первой (внешней) транзакцией:
НачатьТранзакцию(РежимУправленияБлокировкойДанных.Управляемый) не меняет режим внешней транзакции на управляемый.БлокировкаДанных не накладывает исключительные блокировки на уровне СУБД PostgreSQL так, как это ожидает разработчик.Реквизит или измерение Штрихкод обычно имеет строковый тип. Операция МАКСИМУМ в языке запросов 1С при работе со строками транслируется в SQL-запрос с сортировкой по правилам лексикографии СУБД.
Лексикографическое сравнение зависит от настроек языковой локали (collation) PostgreSQL. Если в базе оказывается хотя бы одна запись с нестандартным префиксом, иным количеством символов или пробелами, операция МАКСИМУМ вернет вовсе не тот штрихкод, у которого максимальный числовой номер. В результате расчет ОбщегоНазначения.ПривестиСтрокуКЧислу(ВыбКод) + 1 вернет уже существующий в базе код, что приведет к коллизии.
Рассмотрим несколько вариантов исправления проблемы в зависимости от архитектуры вашего решения.
Если конфигурация или отдельные документы работают в автоматическом или смешанном режиме блокировок, необходимо принудительно заблокировать считываемые строки на уровне СУБД. Для этого в текст запроса выборки добавляется конструкция ДЛЯ ИЗМЕНЕНИЯ:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| МАКСИМУМ(РегШтрихКоды.Штрихкод) КАК Код
|ИЗ
| РегистрСведений.ШтрихКодыДокументов КАК РегШтрихКоды
|ДЛЯ ИЗМЕНЕНИЯ";
В PostgreSQL конструкция ДЛЯ ИЗМЕНЕНИЯ транслируется в инструкции FOR UPDATE / FOR SHARE, что предотвращает параллельную выборку данных другими сеансами до завершения текущей транзакции.
Чтобы управляемая блокировка БлокировкаДанных отработала корректно, необходимо:
Постоянное выполнение запросов с функцией МАКСИМУМ по всей таблице регистра сведений создает высокую нагрузку на СУБД и при блокировке всего регистра задерживает работу параллельных сеансов.
Для предотвращения блокировок рекомендуются следующие альтернативные подходы:
Ссылка.УникальныйИдентификатор(), что полностью избавляет от необходимости блокировать таблицы в СУБД.