142  /  382
Справочник

Вложенные транзакции

Просмотров: 17167
Дата последнего изменения: 21.09.2022
Роберт Басыров
Сложность урока:
3 уровень - средняя сложность. Необходимо внимание и немного подумать.
1
2
3
4
5
Недоступно в лицензиях:
Ограничений нет

"Вложенные" транзакции доступны с версии main 22.200.0.

Что такое транзакция

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

Mysql в движке InnoDB отвечает требованиям ACID к обработке транзакций (в MyISAM не поддерживается):

  • Atomicity - атомарность
  • Consistency - согласованность
  • Isolation - изолированность
  • Durability - надежность

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

Вот классический случай, когда проблема может/должна быть решена транзакцией:

При отправке писем методом CEvent::Send файл может быть не отправлен, если в момент выполнения скрипта будет запущен крон, который отправляет письма. Это происходит потому, что в функции /bitrix/modules/main/lib/mail/event.php запись в таблицу b_event происходит до сохранения файла (58 строка и далее по условию).

Старое правило

Ранее, до обновления main 22.200.0, действовало правило:

В API не должны использоваться функции управления транзакциями базы данных. Это прерогатива вызывающего (конечного) сценария.

Такое правило было потому, что Mysql не поддерживает вложенные транзакции. Могла сложиться ситуация, когда конечный сценарий открывает транзакцию, вызываемый API тоже открывает свою транзакцию, в итоге все коммиты и роллбеки перемешиваются непредсказуемым образом.

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

Вложенные транзакции

Для транзакций нужно использовать API драйвера БД \Bitrix\Main\DB\Connection, методы startTransaction(), commitTransaction() и rollbackTransaction().

Если транзакции вложенные, то повторные старты транзакций создают именованные точки сохранения MySQL SAVEPOINT. Промежуточные коммиты ничего не коммитят. Последний закрывающий коммит коммитит все произведенные с начала первой транзакции изменения.

Сложности возникают, если какие-то из вложенных транзакций захотят откатить изменения в своей транзакции. В этом случае мы попадаем в неопределенную ситуацию, т.к. итоговую цель всех изменений знает только конечный сценарий (см. старое правило).

Поэтому мы действуем в парадигме "вложенные роллбеки не поддерживаются" и даем возможность конечному сценарию решить, как поступить правильно. При наступлении события вложенного роллбека происходит частичный откат к соответствующей точке сохранения ROLLBACK TO SAVEPOINT ... и выбрасывается исключение \Bitrix\Main\DB\TransactionException.

Далее возможны три сценария:

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

Пример кода:

$conn = \Bitrix\Main\Application::getConnection();

$conn->query("truncate table test");

try
{
	$conn->startTransaction();

	$conn->query("insert into test values (1, 'one')");

	// nested transaction
	$conn->startTransaction();

	$conn->query("insert into test values (2, 'two')");

	if (true)
	{
		$conn->commitTransaction();
	}
	else
	{
		$conn->rollbackTransaction();
	}
	// end of nested transaction

	$conn->commitTransaction();
}
catch(\Bitrix\Main\DB\TransactionException $e)
{
	$conn->rollbackTransaction();
}

Новое правило

В итоге новое правило работы с транзакциями выглядит так:

Рекомендуется использовать транзакции в API, чтобы делать атомарные изменения, обеспечивать целостность данных и изолировать изменения. Транзакции должны охватывать непосредственно модификацию данных; эти изменения должны рассматриваться как одна логическая операция. Вложенные транзакции поддерживаются. Тем не менее, вложенные роллбеки должны быть обработаны конечным сценарием и в целом не рекомендуются.

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

5
Курсы разработаны в компании «1С-Битрикс»

Если вы нашли неточность в тексте, непонятное объяснение, пожалуйста, сообщите нам об этом в комментариях.
Развернуть комментарии