При разработке механизмов массовой рассылки или обработки данных в фоновом режиме программисты часто сталкиваются с ситуацией, когда несколько параллельных процессов пытаются одновременно обновить один и тот же объект в базе данных. Типичный пример: отправка писем по списку из табличной части справочника, где каждое фоновое задание должно проставить статус "Отправлено". Рассмотрим подробнее, почему возникают ошибки блокировок и как спроектировать этот процесс правильно.
В представленной ситуации автор столкнулся с двумя критическими ошибками. Разберем их причины, чтобы понять механику работы платформы 1С.
Записать() для одного и того же объекта, СУБД (например, Microsoft SQL Server) не может определить приоритет транзакций. Происходит взаимоблокировка, и одна из транзакций принудительно завершается ошибкой.Ссылка.ПолучитьОбъект(), система запоминает версию этого объекта. Если за время выполнения кода в одном задании другое задание уже успело изменить и записать этот же объект, версия в базе данных обновится. При попытке записи первым заданием платформа увидит, что версия «в руках» уже устарела, и выдаст ошибку: "Операция не может быть выполнена из-за несоответствия версии...".Использование табличной части (ТЧ) справочника для хранения динамических статусов — это архитектурная ловушка. Запись даже одной строки в ТЧ требует перезаписи всего объекта целиком, что блокирует все остальные строки и поля справочника.
Проанализируем, почему регистр сведений (РС) является лучшим выбором для хранения статусов. В отличие от справочника, регистр позволяет записывать данные порционно. Мы можем обновить конкретную запись, не затрагивая другие, и при этом нам не нужно получать объект целиком.
Рассмотрим пример кода для записи статуса в независимый регистр сведений:
МенеджерЗаписи = РегистрыСведений.СтатусыОтправкиПочты.СоздатьМенеджерЗаписи();
МенеджерЗаписи.ОбъектРассылки = СсылкаНаСправочник;
МенеджерЗаписи.ИдентификаторФайла = СписокУдаления.Представление;
МенеджерЗаписи.Статус = Истина;
МенеджерЗаписи.ДатаОбновления = ТекущаяДата();
МенеджерЗаписи.Записать(Истина); // Замещаем только одну запись
В этом случае каждое фоновое задание работает со своей записью в регистре (если измерения уникальны), и конфликтов блокировок не возникает. Это самый надежный способ логирования статусов в многопоточной среде.
Если бизнес-логика требует хранения данных именно в табличной части справочника, разберем, как сделать это правильно. Основная ошибка автора — запись объекта внутри цикла по фоновым заданиям. Вместо этого мы должны собрать все результаты работы и записать объект один раз.
Выясним, как оптимизировать количество заданий. Вместо запуска 6 разных заданий для 6 строк, лучше запустить одно фоновое задание, которое внутри себя в цикле обработает все 6 отправлений. Это существенно снизит накладные расходы системы на создание сеансов.
Пример структуры кода внутри одного фонового задания:
Процедура ВыполнитьРассылку(СсылкаНаОбъект) Экспорт
ОбъектСправочника = СсылкаНаОбъект.ПолучитьОбъект();
Для Каждого СтрокаТЧ Из ОбъектСправочника.ТабличнаяЧастьПК Цикл
Попытка
// Логика отправки почты
ОтправитьПисьмо(СтрокаТЧ.Адрес, СтрокаТЧ.Файл);
СтрокаТЧ.Статус = Истина;
СтрокаТЧ.Отправка = "Успешно";
Исключение
СтрокаТЧ.Статус = Ложь;
СтрокаТЧ.Отправка = КраткоеОписаниеОшибки(ИнформацияОбОшибке());
КонецПопытки;
СтрокаТЧ.ДатаИВремя = ТекущаяДата();
КонецЦикла;
// Записываем объект один раз после обработки всех строк
ОбъектСправочника.Записать();
КонецПроцедуры
Если фоновые задания все же должны быть разделены (например, они выполняют тяжелые вычисления), проанализируем стратегию сбора данных. Каждое задание может возвращать результат своей работы во ВременноеХранилище. Основной поток будет ожидать завершения всех заданий, считывать результаты и обновлять справочник.
Посмотрим на алгоритм действий:
ФоновыеЗадания.ОжидатьЗавершения(МассивЗаданий).Записать().При работе с почтовыми профилями (внутренним и внешним), как упоминал автор, важно правильно обрабатывать исключения. Если отправка через ВнутреннийПрофиль не удалась, это не всегда означает ошибку всей операции — это лишь сигнал к переключению на ВнешнийПрофиль. Только если оба варианта не сработали, стоит фиксировать окончательную ошибку в базе данных.
Также стоит помнить, что в 1С при работе в транзакции блокировки удерживаются до её завершения (КонецТранзакции или завершение процедуры). Поэтому старайтесь минимизировать время нахождения объекта в состоянии «получен для изменения». Метод ПолучитьОбъект() должен вызываться как можно ближе к моменту записи, а тяжелые операции (такие как фактическая отправка письма через SMTP-сервер с ожиданием ответа) лучше выносить за пределы транзакции записи объекта.
Резюме: Чтобы избавиться от ошибок Microsoft SQL Server Native Client: Deadlock и Несоответствие версии, откажитесь от записи объекта справочника из нескольких параллельных потоков. Используйте либо Регистры Сведений для хранения статусов, либо собирайте результаты работы всех потоков в один массив и выполняйте единичную запись объекта в основном сеансе.