加载中...

事务(Transaction)


在Yii中,使用 yii\db\Transaction 来表示数据库事务。

一般情况下,我们从数据库连接启用事务,通常采用如下的形式:

  1. $transaction = $connection->beginTransaction();
  2. try {
  3. $connection->createCommand($sql1)->execute();
  4. $connection->createCommand($sql2)->execute();
  5. // ... executing other SQL statements ...
  6. $transaction->commit();
  7. } catch (Exception $e) {
  8. $transaction->rollBack();
  9. }

在上面的代码中,先是获取一个 yii\db\Transaction 对象,之后执行若干SQL 语句,然后调用之前Transaction 对象的 commit() 方法。这一过程中, 如果捕获了异常,那么调用 rollBack() 进行回滚。

创建事务

在上面代码中,我们使用数据库连接的 beginTransaction() 方法, 创建了一个 yii\db\Trnasaction对象,具体代码在 yii\db\Connection 中:

  1. public function beginTransaction($isolationLevel = null)
  2. {
  3. $this->open();
  4. // 尚未初始化当前连接使用的Transaction对象,则创建一个
  5. if (($transaction = $this->getTransaction()) === null) {
  6. $transaction = $this->_transaction = new Transaction(['db' => $this]);
  7. }
  8. // 获取Transaction后,就可以启用事务
  9. $transaction->begin($isolationLevel);
  10. return $transaction;
  11. }

从创建 Transaction 对象的 new Transaction(['db' => $this]) 形式来看, 这也是Yii一贯的风格。这里简单的初始化了 yii\db\Transaction::db 。

这表示的是当前的 Transaction 所依赖的数据库连接。如果未对其进行初始化, 那么将无法正常使用事务。

在获取了 Transaction 之后,就可以调用他的 begin() 方法,来启用事务。 必要的情况下,还可以指定事务隔离级别。

事务隔离级别的设定,由 yii\db\Schema::setTransactionIsolationLevel() 方法来实现,而这个方法,无非就是执行了如下的SQL语句:

  1. SET TRANSACTION ISOLATION LEVEL ...

对于隔离级别,yii\db\Transaction 也提前定义了几个常量:

  1. const READ_UNCOMMITTED = 'READ UNCOMMITTED';
  2. const READ_COMMITTED = 'READ COMMITTED';
  3. const REPEATABLE_READ = 'REPEATABLE READ';
  4. const SERIALIZABLE = 'SERIALIZABLE';

如果开发者没有给出隔离级别,那么,数据库会使用默认配置的隔离级别。 比如,对于MySQL而言,就是使用 transaction-isolation 配置项的值。

启用事务

上面的代码告诉我们,启用事务,最终是靠调用 Transaction::begin() 来实现的。 那么就让我们来看看他的代码吧:

  1. public function begin($isolationLevel = null)
  2. {
  3. // 没有初始化数据库连接的滚粗
  4. if ($this->db === null) {
  5. throw new InvalidConfigException('Transaction::db must be set.');
  6. }
  7. $this->db->open();
  8. // _level 为0 表示的是最外层的事务
  9. if ($this->_level == 0) {
  10. // 如果给定了隔离级别,那么就设定之
  11. if ($isolationLevel !== null) {
  12. // 设定事务隔离级别
  13. $this->db->getSchema()->setTransactionIsolationLevel($isolationLevel);
  14. }
  15. Yii::trace('Begin transaction' . ($isolationLevel ? ' with isolation level ' . $isolationLevel : ''), __METHOD__);
  16. $this->db->trigger(Connection::EVENT_BEGIN_TRANSACTION);
  17. $this->db->pdo->beginTransaction();
  18. $this->_level = 1;
  19. return;
  20. }
  21. // 以下 _level>0 表示的是嵌套的事务
  22. $schema = $this->db->getSchema();
  23. // 要使用嵌套事务,前提是所使用的数据库要支持
  24. if ($schema->supportsSavepoint()) {
  25. Yii::trace('Set savepoint ' . $this->_level, __METHOD__);
  26. // 使用事务保存点
  27. $schema->createSavepoint('LEVEL' . $this->_level);
  28. } else {
  29. Yii::info('Transaction not started: nested transaction not supported', __METHOD__);
  30. }
  31. // 结合 _level == 0 分支中的 $this->_level = 1,
  32. // 可以得知,一旦调用这个方法, _level 就会自增1
  33. $this->_level++;
  34. }

对于最外层的事务,即当 _level 为 0 时,最终落到PDO的 beginTransaction() 来启用事务。在启用前,如果开发者给定了隔离级别,那么还需要设定隔离级别。

当 _level > 0 时,表示的是嵌套的事务,并非最外层的事务。 对此,Yii使用 SQL 的 SAVEPOINT 和ROLLBACK TO SAVEPOINT 来实现设置事务保存点和回滚到保存点的操作。

嵌套事务

在开头的例子中,展现的是事务最简单的使用形式。Yii还允许把事务嵌套起来使用。 比如,可以采用如下形式来使用事务:

  1. $outerTransaction = $db->beginTransaction();
  2. try {
  3. $db->createCommand($sql1)->execute();
  4. $innerTransaction = $db->beginTransaction();
  5. try {
  6. $db->createCommand($sql2)->execute();
  7. $db->createCommand($sql3)->execute();
  8. $innerTransaction->commit();
  9. } catch (Exception $e) {
  10. $innerTransaction->rollBack();
  11. }
  12. $db->createCommand($sql4)->execute();
  13. $outerTransaction->commit();
  14. } catch (Exception $e) {
  15. $outerTransaction->rollBack();
  16. }

为了实现这一嵌套,Yii使用 yii\db\Transaction::_level 来表示嵌套的层级。 当层级为 0 时,表示的是最外层的事务。

一般情况下,整个Yii应用使用了同一个数据库连接,或者说是使用了单例。 具体可以看 服务定位器(Service Locator) 部分。

而在 yii\db\Connection 中,又对事务对象进行了缓存:

  1. class Connection extends Component
  2. {
  3. // 保存当前连接的有效Transaction对象
  4. private $_transaction;
  5. // 已经缓存有事务对象,且事务对象有效,则返回该事务对象
  6. // 否则返回null
  7. public function getTransaction()
  8. {
  9. return $this->_transaction && $this->_transaction->getIsActive() ? $this->_transaction : null;
  10. }
  11. // 看看启用事务时,是如何使用事务对象的
  12. public function beginTransaction($isolationLevel = null)
  13. {
  14. $this->open();
  15. // 缓存的事务对象有效,则使用缓存中的事务对象
  16. // 否则创建一个新的事务对象
  17. if (($transaction = $this->getTransaction()) === null) {
  18. $transaction = $this->_transaction = new Transaction(['db' => $this]);
  19. }
  20. $transaction->begin($isolationLevel);
  21. return $transaction;
  22. }
  23. }

因此,可以认为整个Yii应用,使用了同一个 Transaction 对象,也就是说, Transaction::_level 在整个应用的生命周期中,是有延续性的。 这是实现事务嵌套的关键和前提。

在这个 Transaction::_level 的基础上,Yii实现了事务的嵌套:

  • 事务对象初始化时,设 _level 为0,表示如果要启用事务, 这是一个最外层的事务。
  • 每当调用 Transaction::begin() 来启用具体事务时, _level 自增1。 表示如再启用事务,将是层级为1的嵌套事务。
  • 每当调用 Transaction::commit() 或 Transaction::rollBack() 时, _level 自减1,表示当前层级的事务处理完毕,返回上一层级的事务中。
  • 当调用了一次 begin() 且还没有调用匹配的 commit() 或 rollBack() , 就再次调用 begin()时,会使事务进行更深一层级的嵌套中。

因此,就有了我们上面代码中,当 _level 为 0 时,需要设定事务隔离级别。 因为这是最外层事务。

而当 _level > 0 时,由于是“嵌套”的事务,一个大事务中的小“事务”,那么, 就使用保存点及其回滚、释放操作,来模拟事务的启用、回滚和提交操作。

要注意,在这一节的开头,我们使用2对嵌套的 try ... catch 来实现事务的嵌套。 由于内层的catch 把可能抛出的异常吞了,不再继续抛出。那么, 外层的 catch ,是捕获不到内层的异常的。

也就是说,这种情况下,外层中的 $sql1 $sql4 不会由于 $sql2 或 $sql3 的失败而中止, $sql1$sql4 可以继续执行并 commit 。

这是嵌套事务的正确使用形式,即内外层之间应当是不相干的。

如果内层事务的异常,会导致外层事务需要回滚,那么我们不应该使用事务嵌套, 而是应该把内外层当成一个事务。这个道理很浅显,但是事实开发中,一个不小心, 就会出昏招。所以,不要动不动就来个 beginTransaction() 。

当然,为了使代码功能有一定的层次感,在必要时,也可以使用嵌套的事务。 但要考虑好,子事务是否真的要吞掉异常?有没有必要继续抛出异常, 使得上一层级的事务也产生回滚?这个要根据实际的情形来确定。

提交和回滚

提交和回滚通过 Transaction::commit() 和 Transaction::rollBack() 来实现:

  1. public function commit()
  2. {
  3. if (!$this->getIsActive()) {
  4. throw new Exception('Failed to commit transaction: transaction was inactive.');
  5. }
  6. // 与begin()对应,只要调用 commit(),_level 自减1
  7. $this->_level--;
  8. // 如果回到了最外层事务,那么应当使用PDO的commit
  9. if ($this->_level == 0) {
  10. Yii::trace('Commit transaction', __METHOD__);
  11. $this->db->pdo->commit();
  12. $this->db->trigger(Connection::EVENT_COMMIT_TRANSACTION);
  13. return;
  14. }
  15. // 以下是尚未回到最外层的情形
  16. $schema = $this->db->getSchema();
  17. if ($schema->supportsSavepoint()) {
  18. Yii::trace('Release savepoint ' . $this->_level, __METHOD__);
  19. // 释放那么保存点
  20. $schema->releaseSavepoint('LEVEL' . $this->_level);
  21. } else {
  22. Yii::info('Transaction not committed: nested transaction not supported', __METHOD__);
  23. }
  24. }
  25. public function rollBack()
  26. {
  27. if (!$this->getIsActive()) {
  28. return;
  29. }
  30. // 调用 rollBack() 也会使 _level 自减1
  31. $this->_level--;
  32. // 如果已经返回到最外层,那么调用 PDO 的 rollBack
  33. if ($this->_level == 0) {
  34. Yii::trace('Roll back transaction', __METHOD__);
  35. $this->db->pdo->rollBack();
  36. $this->db->trigger(Connection::EVENT_ROLLBACK_TRANSACTION);
  37. return;
  38. }
  39. // 以下是未返回到最外层的情形
  40. $schema = $this->db->getSchema();
  41. if ($schema->supportsSavepoint()) {
  42. Yii::trace('Roll back to savepoint ' . $this->_level, __METHOD__);
  43. // 那么就回滚到保存点
  44. $schema->rollBackSavepoint('LEVEL' . $this->_level);
  45. } else {
  46. Yii::info('Transaction not rolled back: nested transaction not supported', __METHOD__);
  47. throw new Exception('Roll back failed: nested transaction not supported.');
  48. }
  49. }

对于提交和回滚:

  • 提交时,会使层级+1,回滚时,会使层级-1
  • 对于最外层的提交和回滚,使用的是数据库事务的 commit 和 rollBack
  • 对于嵌套的内层的提交和回滚,使用的其实是事务保存点的释放和回滚
  • 释放保存点时,会释放保存点的标识符,这个标识符在下次事务嵌套达到这个层级时, 会被再次使用。

有效的事务

在上面的提交、回滚等方法的代码中,我们多次看到了一个 this->getIsActive() 。 这是用于判断当前事务是否有效的一个方法,我们通过它,来看看什么样的一个事务, 算是有效的:

  1. public function getIsActive()
  2. {
  3. return $this->_level > 0 && $this->db && $this->db->isActive;
  4. }

方法很简单明了,一个有效的事务必须同时满足3个条件:

  • _level > 0 。这是由于为0是,要么是刚刚初始化, 要么是所有的事务已经提交或回滚了。也就是说,只有调用过了 begin() 但还没有调用过匹配的 commit() 或 rollBack() 的事务对象,才是有效的。
  • 数据库连接要已经初始化。
  • 数据库连接也必须是有效的。

如果觉得《深入理解Yii2.0》对您有所帮助,也请帮助《深入理解Yii2.0》。 谢谢!


还没有评论.