Статья актуальна для версии платформы 8.3.14
В этой статье я постарался систематизировать статьи ИТС и собственный опыт в плане обхода опасностей, сопровождающих использование транзакций в 1С. Транзакционные блокировки в статье не рассматриваются. К статье приложена демо база. При ее запуске откроется обычное приложение и форма с кнопками для выполнения примеров, на которые ссылается статья серым шрифтом.
Транзакции применяются для целостного изменения связанных данных, т.е. все действия с базой данных, выполняемые в рамках транзакции или выполняются целиком, или целиком откатываются.
- Менеджер транзакции
- Это условное название единого для сеанса базы 1С внутреннего объекта платформы, управляющего транзакцией. Единый для сеанса значит, что он синхронизируется между толстым клиентским и серверным контекстном сеанса.
- Свойства менеджера транзакции
- Глубина/Вложенность/Счетчик - целое число - сколько раз была открыта транзакция минус сколько раз была закрыта
- Отменена – булево - признак отмены транзакции
- Вложенные транзакции
- Менеджер транзакции содержит свойство “Глубина”. Если оно больше 1, то транзакция считается вложенной.
- Логические (1С) и фактические (СУБД) транзакции
- Логическая (1С) транзакция - все операции между началом транзакции и следующим завершением транзакции с тем же значением глубины/вложенности/счетчика транзакций
- Фактическая (СУБД) транзакция - совпадает с логической транзакцией с Глубина = 1
- Вложенными в 1С могут быть только логические транзакции
- При начале логической транзакции увеличивается на 1 свойство “Глубина” менеджера транзакции
- При завершении логической транзакции уменьшается на 1 свойство “Глубина” менеджера транзакции
- Определить во встроенном языке значение свойства Глубина или хотя бы наличие одной вложенности, не изменяя состояние менеджера транзакции, невозможно.
- Вложенность транзакций (ИТС)
- Правила использования транзакций (ИТС)
- Явные и неявные логические транзакции
- Явные - начинаются/завершаются методами встроенного языка
- НачатьТранзакцию
- ЗафиксироватьТранзакцию/ОтменитьТранзакцию
- Неявные - начинаются/завершаются платформой в начале/конце выполнения записи объектов данных. При этом программная запись влияет на свойство Глубина, а интерактивная нет (вероятно ошибка платформы).
- Определить во встроенном языке, является ли транзакция явной/неявной, невозможно.
- Явные - начинаются/завершаются методами встроенного языка
- Сломанные транзакции
- Менеджер транзакции содержит признак “Отменена”. Сбрасывается он только при начале фактической транзакции. Если он установлен, то транзакция считается сломанной и фактическая транзакция подлежит отмене при ее любом завершении. Устанавливается он при возникновении ошибки базы данных и при вызове ОтменитьТранзакцию().
- Определить во встроенном языке, является ли транзакция сломанной, напрямую невозможно, но можно косвенно с достаточной долей уверенности. Пример будет рассмотрен далее.
- Примеры ошибок базы данных
- Ошибка выполнения запроса
- Необработанное исключение при записи объекта
- Отказ при записи объекта (“Не удалось записать <объект>”)
- Ошибка установки транзакционной блокировки
- Ошибка установки объектной блокировки
- Ошибки базы данных и транзакции (ИТС)
- Невосстановимые и восстановимые исключения (ИТС)
- При обращении к БД в сломанной транзакции платформа выбрасывает ошибку “В данной транзакции уже происходили ошибки”. Здесь возникает разрыв между первичной ошибкой, ломающей транзакцию, и этой вторичной ошибкой. Из-за этого разрыва разработчику обычно бывает очень тяжело добраться до причины первичной ошибки. Поэтому к обработке ошибок в сломанной транзакции нужно подходить очень аккуратно. Пример будет рассмотрен далее.
- Работа со ссылками в фактической (СУБД) транзакции
- Кэш представлений ссылок
- Транзакция имеет собственный кэш представлений ссылок.
- Обращение к представлению ссылки может вызывать неявное обращение к БД для обновления кэша представления по этой ссылке.
- Примеры обращений к представлению ссылки
- “” + Ссылка
- Таблица.Сортировать(“Ссылка”) - платформа считывает представления ссылок для сортировки по ним
- ЗаписьЖурналаРегистрации(,,, Ссылка) - всегда получает представление от ссылки
- При обработке исключений в транзакции часто возникает потребность вывода диагностической информации в лог/пользователю. Тут кроется самая коварная особенность сломанной транзакции. При получении представления ссылки с обновлением кэша в сломанной транзакции платформа 8.3.25- (исправлено в 8.3.26) выбрасывает необрабатываемое исключение без указания исходной строки, в которой выполнено это обращение. Причем если код выполняется внутри неявной транзакции, то исключение является восстановимым, а иначе не восстановимым. Об этом очень неудобном поведении я сообщал в 1С в 2013г и в 2019г, но невосстановимость этой ошибки до сих пор осталась. Далее будет приведен безопасный подход к решению задачи.
- Объектный кэш
- Транзакция имеет собственный объектный кэш.
- Обращение к полю ссылки может вызывать неявное обращение к БД для обновления кэша объекта по этой ссылке.
- Примеры обращений к объектному кэшу
- Ссылка.ПометкаУдаления;
- Ссылка.ПолучитьОбъект();
- Аналогично кэшу представлений ссылок при обращении к объектному кэшу в сломанной транзакции платформа может выбрасывать необрабатываемые и невосстановимые исключения, но в меньшем числе ситуаций.
- Кэш представлений ссылок
- Взаимоблокировка (Deadlock)
- Чтобы снизить вероятность появления взаимоблокировок, нужно стараться устанавливать управляемые блокировки, нужные для всех вложенных транзакций, в самом начале фактической (СУБД) транзакции. Тогда выполнение кода установки управляемых блокировок во вложенных транзакциях не будет изменять состав заблокированных ресурсов и тем самым порядок захвата ресурсов в транзакции будет более стабильным и предсказуемым.
- Чтобы снизить вероятность появления взаимоблокировок, нужно стараться устанавливать управляемые блокировки, нужные для всех вложенных транзакций, в самом начале фактической (СУБД) транзакции. Тогда выполнение кода установки управляемых блокировок во вложенных транзакциях не будет изменять состав заблокированных ресурсов и тем самым порядок захвата ресурсов в транзакции будет более стабильным и предсказуемым.
Таблица операций, воздействующих на менеджер транзакции
Операция |
Выброс исключения |
Признак отмены транзакции |
Глубина |
Фактическая транзакция (СУБД) |
Начало записи объекта (неявное начало транзакции) |
Если признак отмены был Истина, то “В данной транзакции уже происходили ошибки” |
Ложь, если глубина была 0 |
+1 при программной записи; не меняется при интерактивной записи |
Открывается, если глубина была 0 |
Конец записи объекта (неявный конец транзакции) |
|
Истина, если Отказ = Истина или выброшено исключение |
-1 при программной записи; не меняется при интерактивной записи |
Если глубина стала 0
|
НачатьТранзакцию |
Ложь, если глубина была 0 |
+1 |
Открывается, если глубина была 0 |
|
ОтменитьТранзакцию |
Если глубина была 0, то “Транзакция неактивна” |
Истина |
-1 |
Откатывается, если глубина стала 0. |
ЗафиксироватьТранзакцию |
Если глубина была 0, то “Транзакция неактивна” |
не меняется |
-1 |
Если глубина стала 0
|
Операция (явная или неявная) с БД в транзакции |
Если признак отмены был Истина, то “В данной транзакции уже происходили ошибки”
|
Истина, если выброшено исключение |
не меняется |
не меняется |
Завершение потока встроенного языка | 0 |
Если глубина была > 0, Откатывается |
Исключение в транзакции
Ошибки в транзакции могут быть
- Ломающие - связаны с БД, выставляют признак "Отменена" менеджера транзакции
- Неломающие - не связаны с БД, не воздействуют на менеджер транзакции
Проверка сломанной транзакции
Хотя явно получить значение признака “Отменена” менеджера транзакции во встроенном языке нельзя. Однако можно воспользоваться косвенным методом - попытаться выполнить простейшую операцию БД (общий модуль ОбщийМодуль1)
// Будет ли выброшено исключение "В данной транзакции уже происходили ошибки" (является ли транзакция сломанной)
Функция ВТранзакцииПроисходилиОшибки() Экспорт
Запрос = Новый Запрос("ВЫБРАТЬ 1");
Попытка
Запрос.Выполнить();
Результат = Ложь;
Исключение
Результат = Истина;
КонецПопытки;
Возврат Результат;
КонецФункции
При необходимости устранить ложные срабатывания, можно будет еще анализировать описание ошибки. Кстати обращения к серверу СУБД этот запрос не производит (только к модели БД).
Подготовка данных для обработки исключения в сломанной транзакции
В ряде случаев имеет смысл готовить данные для обработки исключения заранее, т.е. до выполнения опасной операции с БД, которая может сломать транзакцию. Особенно актуально это для предотвращения обращений к объектному кэшу и кэшу представлений, которые могут вызывать невосстановимые ошибки в сломанной транзакции в случае необходимости считывания данных в этот кэш. Также в рамках этой подготовки можно наполнить объектный кэш и кэш представлений ссылок нужными ссылками, но делать это нужно в транзакции. Тут важно найти оптимальный баланс между дополнительными подготовительными обращениями к БД и диагностической ценностью этих данных для обработки потенциальных исключений. В случае риска обращений к кэшу представлений для регистрации диагностической информации вместо подготовки данных можно в качестве альтернативы использовать идентификаторы ссылок.
Передача ссылки в журнал регистрации в сломанной транзакции
При обработке исключения в сломанной транзакции часто разумно писать диагностическую информацию в журнал регистрации. При этом метод ЗаписьЖурналаРегистрации() неявно берет представление от ссылки, используя кэш представлений ссылок, и помещает его в поле "Представление данных" события журнала. Обращение к этому кэшу в сломанной транзакции несет риск невосстановимой ошибки на платформе 8.3.25-. Поэтому рекомендую передавать ссылку в журнал регистрации только через эту функцию (общий модуль ОбщийМодуль1)
// Помещение представления ссылки в кэш представлений в сломанной транзакции вызывает досрочное завершение фактической транзакции с невосстановимой ошибкой "В данной транзакции уже происходили ошибки"
// Передача в метод ЗаписьЖурналаРегистрации() ссылки на объект в сломанной транзакции приведет к такому исключению, если представление будет обновлено в кэше.
// Поэтому в таком случае функция возвращает строку идентификатор ссылки. Подразумевается что такие места в коде будут устраняться и потому возвращаться идентификатор будет достаточно редко.
Функция СсылкаДляПередачиВЖурналРегистрации(Ссылка) Экспорт
Если ВТранзакцииПроисходилиОшибки() Тогда
Результат = "" + Ссылка.УникальныйИдентификатор();
Иначе
Результат = Ссылка;
КонецЕсли;
Возврат Результат;
КонецФункции
Тогда безопасная запись в журнал регистрации при обработке исключения в транзакции может выглядеть так
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации("МойОшибка", УровеньЖурналаРегистрации.Ошибка, Ссылка.Метаданные(), СсылкаДляПередачиВЖурналРегистрации(Ссылка));
...
КонецПопытки;
Открывать и закрывать транзакцию в одном методе
Старайтесь открывать и закрывать логическую транзакцию в одном методе. Это значительно облегчает отладку и анализ кода. Следствие этой рекомендации - выполнять код транзакции в попытке, чтобы в исключении можно было отменить транзакцию. Иначе исключение поднимется по стеку (в вызывающий метод) без закрытия логической транзакции (восстановления свойства Глубина менеджера транзакции) в текущем методе. Часто и особенно во вложенных транзакциях при обработке такого исключения и после возможной регистрации диагностической информации исключение перевыбрасывают. Даже если на текущий момент ваша транзакция не вызывается вложенно потом это может измениться. Поэтому разумно сразу об этом позаботиться.
Обработка исключения при записи в попытке
Одним из частых вариантов превращения транзакции в сломанную является запись объекта в БД в попытке без транзакции. Допустим есть корректно работающий метод, выполняющий запись документа с регистраций в журнал регистрации ошибки, если такая возникает, но без перевыброса исключения. Если этот метод будет вызван внутри неявной транзакции и в нем возникнет исключение, то при попытке фиксации этой внешней транзакции будет выброшено исключение "В данной транзакции уже происходили ошибки". Поэтому при заворачивании операции с БД в попытку рекомендую в обработке исключения в конце вставлять перевыброс исключения в случае наличия транзакции.
Попытка
Объект.Записать();
Исключение
ЗаписьЖурналаРегистрации("ОшибкаЗаписи", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
Если ТранзакцияАктивна() Тогда
ВызватьИсключение;
КонецЕсли;
Результат = Ложь;
КонецПопытки;
Операции с БД в сломанной транзакции
Иногда бывает нужно в сломанной транзакции (например при обработке исключения) выполнить очень важную операцию с БД.
В таком случае может быть оправдано пренебречь рекомендацией "Открывать и закрывать транзакцию в одном методе" и завершить фактическую транзакцию в текущем месте, чтобы сделать возможным выполнение этой важной операции с БД.
Исключение
Пока ТранзакцияАктивна() Цикл
ОтменитьТранзакцию();
КонецЦикла;
ЗаписатьИнформациюОбОшибкеВРегистр();
...
КонецПопытки;
Следует иметь ввиду, что такой прием повышает вероятность возникновения ошибок во внешних транзакциях. Самый негативный сценарий - текущая транзакция была вложена в неявную фактическую транзакцию записи объекта. Тогда при завершении записи объекта будет невосстановимое исключение (кнопка "Невосстановимая ошибка после отмены транзакции в неявной транзакции").
Также для выполнения очень важной операции с БД в сломанной транзакции можно использовать запуск фонового задания.
Рекомендуемая структура кода транзакции
-
метод НачатьТранзакцию рекомендуется располагать за пределами блока Попытка-Исключение непосредственно перед оператором Попытка;
-
все действия, выполняемые после вызова метода НачатьТранзакцию, должны находиться в одном блоке Попытка, в том числе чтение, блокировка и обработка данных;
-
метод ЗафиксироватьТранзакцию рекомендуется располагать последним в блоке Попытка перед оператором Исключение, чтобы гарантировать, что после ЗафиксироватьТранзакцию не возникнет исключение;
-
необходимо предусмотреть обработку исключений – в блоке Исключение нужно сначала вызвать метод ОтменитьТранзакцию, а затем выполнять другие действия, если они требуются;
-
рекомендуется в блоке Исключение делать запись в журнал регистрации;
-
в конце блока Исключение рекомендуется добавить оператор ВызватьИсключение
Пример (кнопка "Обработка ошибки в явной транзакции")
Процедура ОбработкаОшибкиВЯвнойТранзакции1() Экспорт
НачатьТранзакцию(); // Уровень = 1
Попытка
Ссылка = Справочники.БезОбработчиков.Тест1;
ПредставлениеСсылки = "" + Ссылка; // Подготовка данных для возможной обработки исключения. Это может быть неоправданное обращение к БД.
НачатьТранзакцию(); // Уровень = 2
Попытка
Запрос = Новый Запрос("ВЫБРАТЬ 1/0");
Запрос.Выполнить();
ЗафиксироватьТранзакцию(); // Уровень = 2
Исключение
ОтменитьТранзакцию(); // Уровень = 2
//ЗаписьЖурналаРегистрации("МойОшибка", УровеньЖурналаРегистрации.Ошибка, Ссылка.Метаданные(), Ссылка); // Так получим невосстановимую ошибку, если представления ссылки будет обновлено в кэше
ЗаписьЖурналаРегистрации("МойОшибка", УровеньЖурналаРегистрации.Ошибка, Ссылка.Метаданные(), СсылкаДляПередачиВЖурналРегистрации(Ссылка));
Сообщить("Обработана ошибка БД - пытались записать """ + ПредставлениеСсылки + """");
ВызватьИсключение;
КонецПопытки;
ЗафиксироватьТранзакцию(); // Уровень = 1
Исключение
ОтменитьТранзакцию(); // Уровень = 1
Сообщить("Обработана ошибка БД");
ВызватьИсключение;
КонецПопытки;
КонецПроцедуры
Шаблон транзакции
Создайте себе шаблон текста транзакции (команда "Сервис"/"Шаблоны текста" конфигуратора)
НачатьТранзакцию();
Попытка
<?>
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
// Сюда писать код обработки ошибки
ВызватьИсключение;
КонецПопытки;
Скрытая отмена транзакции
Этот пример демонстрирует, что даже без вызова ОтменитьТранзакцию() явная фактическая транзакция может быть отменена. При завершении при установленном признаке Отменена явная транзакция молча откатывается в отличие от неявной транзакции, которая выбрасывает исключение "В данной транзакции уже происходили ошибки". Не рекомендую применять это, однако рекомендую изучить для лучшего понимая всех тонкостей работы транзакций (кнопка "Скрытая отмена транзакции")
НачатьТранзакцию();
Попытка
Объект = Справочники.БезОбработчиков.тест1.ПолучитьОбъект();
Объект.Наименование = ТекущаяДата();
Объект.Записать();
Запрос = Новый Запрос("ВЫБРАТЬ 1/0");
Запрос.Выполнить(); // Ошибка возникла при выполнении операции БД, поэтому установлен признак "Отменена" менеджера транзакции
Исключение
Сообщить("Обработана ошибка БД");
КонецПопытки;
ЗафиксироватьТранзакцию(); // Отмена фактической транзакции, т.к. Глубина стала 0 и Отменена = Истина
В ряде случаев может быть полезным выбрасывать исключение в такой ситуации по аналогии с неявной транзакцией. Для этого вместо вызова штатного метода предлагаю использовать собственный метод ЗафиксироватьТранзакциюОбязательно()
Процедура ЗафиксироватьТранзакциюОбязательно() Экспорт
Если ВТранзакцииПроисходилиОшибки() Тогда
ВызватьИсключение "В данной транзакции уже происходили ошибки";
Иначе
ЗафиксироватьТранзакцию();
КонецЕсли;
КонецПроцедуры
Другим примером скрытой отмены транзакции является завершение потока встроенного языка, которое происходит при
- завершение сеанса
- завершение клиентским приложением обработки команды пользователя
Разрыв неявной транзакции
Этот пример демонстрирует довольно неочевидную возможность разорвать фактическую транзакцию неинтерактивной записи объекта. Не рекомендую применять это без тщательной подготовки, однако рекомендую изучить для лучшего понимая всех тонкостей работы транзакций (кнопка "Разрыв записи объекта", справочник РазрывЗаписиОбъекта модуль Объекта)
Процедура ПередЗаписью(Отказ)
ЭтотОбъект.Наименование = ТекущаяДата();
А = Справочники.БезОбработчиков.тест1.ПолучитьОбъект();
А.Реквизит1 = ТекущаяДата();
А.Записать();
Попытка
Запрос = Новый Запрос("ВЫБРАТЬ 1/0");
Запрос.Выполнить(); // Ошибка возникла при выполении операции БД, поэтому установлен признак "Отменена" менеджера транзакции
Исключение
КонецПопытки;
Если ОбщийМодуль1.ВТранзакцииПроисходилиОшибки() Тогда
ЗафиксироватьТранзакцию(); // Уменьшили внутреннее свойство "Глубина". Оно стало 0 и установлен признак "Отменена", поэтому фактическая транзакция отменилась.
// Завершены неявная логическая и фактическая транзакции. Транзакционные блокировки удалены. Изменения объекта ЭтотОбъект пока остались только в памяти.
КонецЕсли;
// Выполняется вне фактической транзакции
А = Справочники.БезОбработчиков.Тест1.ПолучитьОбъект();
А.Наименование = ТекущаяДата();
А.Записать();
// Изменения объекта А уже зафиксированы, т.к. сделаны вне фактической транзакции
Если Не ТранзакцияАктивна() Тогда
НачатьТранзакцию(); // Откроем явную логическую и фактическую транзакции, чтобы внутренная логика фиксации транзакции метода Записать() объекта данных не выбросила исключение
КонецЕсли;
КонецПроцедуры
Если же попробовать сделать тот же фокус с интерактивной записью объекта, то на вызове ЗафиксироватьТранзакцию() будет выброшено исключение "Транзакция не активна" (вероятно ошибка платформы). На базе этой особенности можно сделать Самодельный обработчик ПослеЗаписи объекта.
Поиск установки признака отмены транзакции
Как было показано выше, установленный признак отмены транзакции может приводить к различным трудно диагностируемым классическими средствами ошибкам дальше по трассе кода. Поэтому важно иметь способ быстро находить место в коде, где установился этот признак.
Тут нам поможет техножурнал. Достаточно включить на сервере приложений сбор события SDBL с отбором Func='setRollbackOnly'. В инструменте "Настройка техножурнала (ИР)" из подсистемы Инструменты разработчика регистрация этого события входит в шаблон "Дежурный"
Пример регистрации такого события при выполнении кода на толстом клиенте:
НачатьТранзакцию();
НачатьТранзакцию();
ОтменитьТранзакцию();
26:06.042020-1,SDBL,4,process=rphost,p:processName=KA1,OSThread=968,t:clientID=973,t:applicationName=1CV8,t:computerName=CORTEX,t:connectID=1190,SessionID=4,Usr=1,DBMS=DBMSSQL,DataBase=cortex\ka1,Trans=1,Func=setRollbackOnly
26:06.058001-0,Context,3,process=rphost,p:processName=KA1,OSThread=968,t:clientID=973,t:applicationName=1CV8,t:computerName=CORTEX,t:connectID=1190,SessionID=4,Usr=1,Context='
ОбщийМодуль.Модуль1.Модуль : 3 : ОтменитьТранзакцию();'
При выполнении кода на сервере на платформе 8.3.18 при вызове метода ОтменитьТранзакцию() мне не удалось добиться регистрации такого события. В 1С признали ошибку платформы и исправили в 8.3.20. Пример регистрации такого события при выполнении кода на сервере:
НачатьТранзакцию();
Запрос = Новый Запрос("ВЫБРАТЬ 1/0");
Попытка
Запрос.Выполнить()
Исключение
КонецПопытки;
47:00.215002-1,SDBL,5,process=rphost,p:processName=KA1,OSThread=1948,t:clientID=1687,t:applicationName=1CV8,t:computerName=CORTEX,t:connectID=1202,SessionID=4,Usr=1,DBMS=DBMSSQL,DataBase=cortex\ka1,Trans=1,Func=setRollbackOnly,Context='
ОбщийМодуль.Модуль1.Модуль : 4 : Запрос.Выполнить()'
Полезные статьи по теме
- Правила использования транзакций (ИТС)
- Особенности работы объектов при отмене транзакции (ИТС)
- Ошибки базы данных и транзакции (ИТС)
- Невосстановимые и восстановимые исключения (ИТС)
- Вложенность транзакций (ИТС)
- Вы не умеете работать с транзакциями (habr)