Skip to main content

Механизм транзакций

Статья на ИТС

Транзакция – это последовательность действий, переводящая базу данных из одного целостного состояния в другое целостное состояние. 

Свойства транзакции:

  • Атомарность (неделимость). После завершения транзакции все данные должны быть согласованы. Даже при простом добавлении записи может произойти рассогласование, и необходимо это обрабатывать.
  • Изоляция. Это свойство обеспечивает параллельную работу пользователей и предотвращает порчу общих данных. Например, чтобы не вышло ситуации, когда 2 пользователя меняют один и тот же документ и тем самым перестирают данные друг друга. Блокировки выступают как средство обеспечения изоляции транзакции.

Особенности транзакции:

  • не поддерживаются вложенные транзакции. Так эта особенность официально звучит, однако интересно: далее в тексте встречается термин Вложенная транзакция. Понять и простить. Под этим подразумевается, что вместо дерева транзакций мы имеем дело с одноуровневым списком, и все должно быть выполнено. В случае возникновения  и подавлении ошибки где-то внутри "вложенной" транзакции, все равно вся транзакция будет считаться битой. 

    Здесь помогает метод ТранзакцияАктивна, помогающий текущему коду понять, находится ли он внутри транзакции или его транзакция наверху.

  • при возникновении исключения в общем случае транзакция не может быть зафиксирована – при этом не важно, было ли это исключение обработано или нет

  • транзакция может быть инициирована явно в прикладном коде при использовании метода НачатьТранзакцию. Так же платформа неявным образом начинает транзакцию при любой записи в базу данных //Добавлено после экспериментов// Некоторые события (например ПриЗаписиНаСервере) входят в состав транзакции, автоматически созданной платформой. Поэтому в некоторых источниках вся процедура обработки данного события входит в транзакцию (причем код, начинающий и заканчивающий транзакцию где-то посередине), что приводит к неправильным выводам (в моем случае - "то есть если в коде присутствует начало и конец транзакции, то вся процедура попадает в транзакцию", "а зачем тогда посередине начинать транзакцию?").

Из особенностей следуют правила:

  • Поскольку исключение не отменяет транзакцию сразу, но запрещает успешное завершение транзакции, то все вызовы НачатьТранзакцию с одной стороны и ЗафиксироватьТранзакцию или ОтменитьТранзакцию с другой стороны должны быть парными.
  • Начало транзакции и ее фиксация (отмена) должны происходить в контексте одного метода
  • Обработка исключений должна придерживаться следующих правил:

    • метод НачатьТранзакцию должен быть за пределами блока Попытка-Исключение непосредственно перед оператором Попытка

    • все действия, выполняемые после вызова метода НачатьТранзакцию, должны находиться в одном блоке Попытка, в том числе чтение, блокировка и обработка данных

    • метод ЗафиксироватьТранзакцию должен идти последним в блоке Попытка перед оператором Исключение, чтобы  гарантировать, что после ЗафиксироватьТранзакцию не возникнет исключение

    • необходимо предусмотреть обработку исключений – в блоке Исключение нужно сначала вызвать метод ОтменитьТранзакцию, а затем выполнять другие действия, если они требуются

    • рекомендуется в блоке Исключение делать запись в журнал регистрации

    • при использовании вложенных транзакций в конце блока Исключение рекомендуется добавить оператор ВызватьИсключение.  В противном случае исключение не будет передано выше по стеку вызовов, там не сработает обработка исключения, внешняя транзакция не будет явным образом отменена и платформа вызовет исключение «В данной транзакции происходила ошибка»  

      НачатьТранзакцию();
      Попытка
          БлокировкаДанных = Новый БлокировкаДанных;
          ЭлементБлокировкиДанных = БлокировкаДанных.Добавить("Документ.ПриходнаяНакладная");
          ЭлементБлокировкиДанных.УстановитьЗначение("Ссылка", СсылкаДляОбработки);
          ЭлементБлокировкиДанных.Режим = РежимБлокировкиДанных.Исключительный;
          БлокировкаДанных.Заблокировать();
          ... // чтение или запись данных
          ДокументОбъект.Записать();
          ЗафиксироватьТранзакцию();
      Исключение
          ОтменитьТранзакцию();
          ЗаписьЖурналаРегистрации(НСтр("ru = 'Выполнение операции'"),
              УровеньЖурналаРегистрации.Ошибка,,,
              ОбработкаОшибок.ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
          ВызватьИсключение; // есть внешняя транзакция
      КонецПопытки;
  • Использование вложенных транзакций приводит к усложнению кода. Принимая решение об использовании этой возможности, нужно очень взвешенно оценить решаемую задачу: возможно, это усложнение просто не оправдано.

    • Не стоит усложнять код, явно используя метод НачатьТранзакцию, когда кроме записи объекта другие действия c базой данных не делаются – платформа при записи сама откроет транзакцию.

    • Не нужно явно открывать транзакцию тогда, когда не требуется выполнять ответственное чтение данных. Например, обычно ответственное чтение не требуется при записи нового объекта (нового набора записей регистра).

    • При использовании методов ПолучитьОбъект (или Прочитать для наборов записей) необходимо анализировать должно ли чтение быть отвественным и в зависимости от этого принимать решение о явном использовании метода НачатьТранзакцию.

    •  Если метод рассчитан на вызов только в рамках уже открытой транзакции (например, метод предназначен для вызова только из событий ПередЗаписью, ОбработкаПроведения и т.п.) в общем случае явным образом открывать в нем транзакцию не имеет никакого практического смысла.

    • При необходимости повысить качество сообщений об ошибках – на каждом уровне разработчик может предусмотреть свою обработку исключений, для чего, возможно, потребуется открыть вложенную транзакцию.

      Пример. Вызывается метод ДобавитьЭлектроннуюПодпись. Внутри, если что-то пошло не так, нужно обработать исключение и добавить текст вида: «Не удалось добавить электронную подпись к объекту %ПредставлениеОбъекта% по причине:%ОписаниеОшибки%». В противном случае исключение будет обработано выше по стеку вызовов, например, при записи файла и будет выдано сообщение вида: «Не удалось записать файл %ИмяФайла% по причине: %ОписаниеОшибки%», где в «%ОписаниеОшибки%», будет просто указание на строчку кода и пользователю будет непонятно, зачем вообще программа записывала файл, если он просто его подписывал.

    • При обработке исключения, если транзакция все еще активна, например, исключение возникло во вложенной транзакции, нельзя обращаться к базе данных, так как это приведет к исключению «В этой транзакции уже происходили ошибки». При этом нужно учитывать, что обращение к базе данных может быть неявным, например, для получения представления ссылки.

  • В общем случае в рамках одной транзакции нужно выполнять только те действия, которые неделимы, исходя из бизнес-логики.

    Пример. При проведении документа записывается документ и его движения в регистрах. Если не прошла запись хотя бы в один регистр вся операция проведения должна быть отменена.

  • Следует избегать транзакций, которые выполняются длительное время.

    Например, неправильно: для загрузки адресного классификатора записывать все данные, относящиеся к одной версии классификатора в одной транзакции, для того, чтобы в случае ошибки откатить целиком загружаемую версию классификатора. Т.к. данных по одной версии классификатора много (объем около 1 Гб), то для выполнения такой транзакции, во-первых, может не хватить оперативной памяти (особенно при использовании файловой информационной базы на 32-разрядной ОС), а, во-вторых, такая операция будет выполняться достаточно долго и ее нельзя будет оптимизировать за счет выполнения в несколько потоков.

    Правильно: разбить загрузку новой версии классификатора на небольшие порции так, чтобы запись порции в одной транзакции не превышала 20 секунд в условиях высоконагруженной информационной системы и реализовать функциональность по откату к предыдущей версии в случае ошибки. Максимальная продолжительность указана исходя из того, что время ожидания установки транзакционной блокировки данных в информационной базе по умолчанию равно 20 сек.

  • Чем дольше выполняется транзакция, тем большее время будут заняты ресурсы сервера 1С:Предприятия и СУБД. Как правило длинные транзакции занимают следующие ресурсы:

    • в ходе выполнения транзакции все изменения в базе данных записываются в журнал транзакций, что необходимо для возможности откатить транзакцию;

    • блокировки, установленные в транзакции, остаются до конца транзакции

    • на сервере 1С:Предприятия блокировки занимают оперативную память

    • другие ресурсы, необходимые самой бизнес-логике, которая выполняется в транзакции.

  • Если две транзакции пересекаются по блокируемым ресурсам, то транзакция, которая начала выполняться позже, будет ожидать возможность установления блокировки ограниченное время (по умолчанию – 20 секунд), после чего будет завершена с исключением «Превышено время ожидания установки блокировки». Поэтому длинные транзакции могут сильно снижать удобство параллельной работы пользователей.

    Возникновение таких исключений – это повод провести анализ действий, которые выполняются в конфликтующих транзакциях

  • В рамках транзакции нужно стремиться выполнять минимум действий – только те, которые нельзя в соответствии с бизнес-логикой выполнять вне транзакции.

  • Обязательное использование транзакции в случае, если для ускорения операции записи в регистр используется отключение итогов, такую операцию вместе с отключением и включением итогов необходимо выполнять в транзакции, иначе в других сеансах может возникнуть ошибка при получении среза последних.

Проверка понимания.

Определение и граница использования транзакции. Транзакции - нечто неделимое, возвращающее все в предыдущее состояние. Это осталось в голове после заумных текстов без тестов лапками. Первым, что пришло в голову - восстановить значение реквизита :) Обработка с полем ввода ТестовоеЧисло, 0 по умолчанию.

&НаСервере
Процедура БезОбработкиОшибокНаСервере()
	НачатьТранзакцию();
	ТестовоеЧисло = 5;
	ВызватьИсключение "Число не то!";
	ЗафиксироватьТранзакцию();
КонецПроцедуры

&НаКлиенте
Процедура БезОбработкиОшибок(Команда)
	БезОбработкиОшибокНаСервере();
КонецПроцедуры

Не удалось! Ошибка сгенерировалась, но ТестовоеЧисло стало 5. Ведь транзакции актуальны при изменении данных в базе, т е SQL запрос обрамляется чем-то типа "BEGIN ... COMMIT". Мой косяк, поехали дальше.

Ошибка при создании элемента справочника. Справочник Города. 

&НаСервереБезКонтекста
Процедура НовыйЭлементСправочникаНаСервере()
	НачатьТранзакцию();
	НовыйГород = Справочники.Города.СоздатьЭлемент();
	НовыйГород.Наименование = "Тестовый город";
	НовыйГород.Записать();
    НовыйГород2 = Справочники.Города.СоздатьЭлемент();
	НовыйГород2.Наименование = "Тестовый город 2";
	НовыйГород2.Записать();
	ВызватьИсключение "Город не правильный!";
	ЗафиксироватьТранзакцию();
КонецПроцедуры

Да, тестовые города не создались. Получилось! 

Вложенные транзакции (или то чего нет). 

&НаСервереБезКонтекста
Процедура ВложеннаяТранзакция()
	НачатьТранзакцию();
	Попытка
		НовыйГород = Справочники.Города.СоздатьЭлемент();
		НовыйГород.Наименование = "Тестовый город вложенный";
		НовыйГород.Записать(); 
		ВызватьИсключение "Город не правильный!";
		ЗафиксироватьТранзакцию();
	Исключение
		ОтменитьТранзакцию();
	КонецПопытки;
КонецПроцедуры

&НаСервереБезКонтекста
Процедура ВложенныеТарнзакцииНаСервере()
	НачатьТранзакцию();
	НовыйГород = Справочники.Города.СоздатьЭлемент();
	НовыйГород.Наименование = "Тестовый город внешний";
	НовыйГород.Записать(); 
	ВложеннаяТранзакция();
	ЗафиксироватьТранзакцию();   
КонецПроцедуры

Во вложенной транзакции мы подавляем исключение - и вся транзакция тихо не выполняется. Ни один город не создан. Я не получил вообще никакого отклика от платформы, что и логично. Теперь попробуем добавить во внешний код обработку ошибок. 

&НаСервереБезКонтекста
Процедура ВложеннаяТранзакция()
	НачатьТранзакцию();
	Попытка
		НовыйГород = Справочники.Города.СоздатьЭлемент();
		НовыйГород.Наименование = "Тестовый город вложенный";
		НовыйГород.Записать(); 
		ВызватьИсключение "Город не правильный!";
		ЗафиксироватьТранзакцию();
	Исключение
		ОтменитьТранзакцию();
	КонецПопытки;
КонецПроцедуры

&НаСервереБезКонтекста
Процедура ВложенныеТарнзакцииНаСервере()
	НачатьТранзакцию();
	Попытка
		НовыйГород = Справочники.Города.СоздатьЭлемент();
		НовыйГород.Наименование = "Тестовый город внешний";
		НовыйГород.Записать(); 
		ВложеннаяТранзакция();
		ЗафиксироватьТранзакцию();
	Исключение
		ОтменитьТранзакцию();
		Сообщить("Исключение было поймано");
	КонецПопытки;
КонецПроцедуры

Вполне логично, что сообщение не было сформировано.

Проверка функции ТранзакцияАктивна. 

&НаСервереБезКонтекста
Процедура НовыйЭлементСправочникаНаСервере() 
	Сообщить(ТранзакцияАктивна());
	НачатьТранзакцию();
	НовыйГород = Справочники.Города.СоздатьЭлемент();
	НовыйГород.Наименование = "Тестовый город 3";
	НовыйГород.Записать(); 
	Сообщить(ТранзакцияАктивна());
	ЗафиксироватьТранзакцию();    
	Сообщить(ТранзакцияАктивна());
КонецПроцедуры

Ожидаемый вывод: Нет, Да, Нет. Теперь проверим факт из статьи о блокировках о возникновении блокировки и как следствие транзакции при чтении (это так я понял текст) 

&НаСервереБезКонтекста
Процедура ТранзакцияПриЧтенииНаСервере()
	МойГород = Справочники.Города.НайтиПоНаименованию("Иркутск");
	Сообщить(ТранзакцияАктивна());
	НовыйГород = Справочники.Города.СоздатьЭлемент();
	НовыйГород.Наименование = "Тестовый город 3";
	НовыйГород.Записать();
	Сообщить(ТранзакцияАктивна());
КонецПроцедуры

&НаКлиенте
Процедура ТранзакцияПриЧтении(Команда)
	ТранзакцияПриЧтенииНаСервере();
КонецПроцедуры

И получил Нет, Нет. Видимо что-то не так понял. Аналогичный результат при поэлементном чтении всего справочника. После внимательного рассмотрения, о транзакции в пределах процедуры было сказано для обработки события ПриЗаписиНаСервере (в случае сохранения данных через форму). Чтож, проверим это. Если транзакция есть, то данный код не запишет данные.

&НаСервере
Процедура ПриЗаписиНаСервере(Отказ, ТекущийОбъект, ПараметрыЗаписи)
	Отказ = ТранзакцияАктивна(); 
КонецПроцедуры

И да, создать объект не удалось, значит транзакция была активна!

Количество выполненных действий при исключении во вложенной транзакции. Смоделируем ситуацию: при входе во вложенную транзакцию происходит подавленная ошибка. Причем только одна ошибка в середине процесса. Сколько раз выполнится внешняя транзакция? Ведь в одном из предыдущих примеров исключение из вложенной транзакции поймано не было. 

&НаСервереБезКонтекста
Процедура ВложеннаяТранзакция()
	НачатьТранзакцию();
	Попытка
		НовыйЭлемент = Справочники.ДляТранзакций.СоздатьЭлемент();
		НовыйЭлемент.Наименование = "Тестовый элемент внутренний";
		НовыйЭлемент.Записать();
		ВызватьИсключение "Ошибка!";
		ЗафиксироватьТранзакцию();
	Исключение
		ОтменитьТранзакцию();
		Сообщить("Ошибка во вложенной транзакции");
	КонецПопытки;
КонецПроцедуры

&НаСервереБезКонтекста
Процедура ТестТранзакцииНаСервере()
	НачатьТранзакцию();
	Попытка
		Для сч = 1 По 5 Цикл
			НовыйЭлемент = Справочники.ДляТранзакций.СоздатьЭлемент();
			НовыйЭлемент.Наименование = "Тестовый элемент внешний " + Строка(сч);
			НовыйЭлемент.Записать(); 
			Сообщить("Попытка записи " + Строка(сч) + " внешнего элемента.");     
			//Если сч = 3 Тогда
			//	ВложеннаяТранзакция(); //ошибка в середине
			//КонецЕсли;
		КонецЦикла;  
		ВложеннаяТранзакция();//ошибка в конце
		ЗафиксироватьТранзакцию();
	Исключение
		ОтменитьТранзакцию();
		Сообщить("Исключение было поймано");
	КонецПопытки;
КонецПроцедуры

Сначала все логично. В указанном варианте (он повторяет предыдущий эксперимент) выведено 5 сообщений и "Ошибка во вложенной транзакции". Сообщение "Исключение было поймано" не отображается. 

А дальше ждал сюрприз. Если раскомментировать условие (ошибка на третьем шаге) то получается какая-то бабуйня: выведено 3 сообщения, сообщение "Ошибка во вложенной транзакции" и сообщение "Исключение было поймано"! То есть исключение транслируется наверх, если происходит между выполняемыми действиями. Однако если после выполняемых действий, но до фиксации транзакции - ошибка не транслируется наверх, а транзакция тихо откатывается.