事务(Transaction)

最后更新于:2022-04-01 02:20:13

在Yii中,使用 yii\db\Transaction 来表示数据库事务。 一般情况下,我们从数据库连接启用事务,通常采用如下的形式: ~~~ $transaction = $connection->beginTransaction(); try { $connection->createCommand($sql1)->execute(); $connection->createCommand($sql2)->execute(); // ... executing other SQL statements ... $transaction->commit(); } catch (Exception $e) { $transaction->rollBack(); } ~~~ 在上面的代码中,先是获取一个 yii\db\Transaction 对象,之后执行若干SQL 语句,然后调用之前Transaction 对象的 commit() 方法。这一过程中, 如果捕获了异常,那么调用 rollBack() 进行回滚。 ## 创建事务[](http://www.digpage.com/transaction.html#id2 "Permalink to this headline") 在上面代码中,我们使用数据库连接的 beginTransaction() 方法, 创建了一个 yii\db\Trnasaction对象,具体代码在 yii\db\Connection 中: ~~~ public function beginTransaction($isolationLevel = null) { $this->open(); // 尚未初始化当前连接使用的Transaction对象,则创建一个 if (($transaction = $this->getTransaction()) === null) { $transaction = $this->_transaction = new Transaction(['db' => $this]); } // 获取Transaction后,就可以启用事务 $transaction->begin($isolationLevel); return $transaction; } ~~~ 从创建 Transaction 对象的 new Transaction(['db' => $this]) 形式来看, 这也是Yii一贯的风格。这里简单的初始化了 yii\db\Transaction::db 。 这表示的是当前的 Transaction 所依赖的数据库连接。如果未对其进行初始化, 那么将无法正常使用事务。 在获取了 Transaction 之后,就可以调用他的 begin() 方法,来启用事务。 必要的情况下,还可以指定事务隔离级别。 事务隔离级别的设定,由 yii\db\Schema::setTransactionIsolationLevel() 方法来实现,而这个方法,无非就是执行了如下的SQL语句: ~~~ SET TRANSACTION ISOLATION LEVEL ... ~~~ 对于隔离级别,yii\db\Transaction 也提前定义了几个常量: ~~~ const READ_UNCOMMITTED = 'READ UNCOMMITTED'; const READ_COMMITTED = 'READ COMMITTED'; const REPEATABLE_READ = 'REPEATABLE READ'; const SERIALIZABLE = 'SERIALIZABLE'; ~~~ 如果开发者没有给出隔离级别,那么,数据库会使用默认配置的隔离级别。 比如,对于MySQL而言,就是使用 transaction-isolation 配置项的值。 ## 启用事务[](http://www.digpage.com/transaction.html#id3 "Permalink to this headline") 上面的代码告诉我们,启用事务,最终是靠调用 Transaction::begin() 来实现的。 那么就让我们来看看他的代码吧: ~~~ public function begin($isolationLevel = null) { // 没有初始化数据库连接的滚粗 if ($this->db === null) { throw new InvalidConfigException('Transaction::db must be set.'); } $this->db->open(); // _level 为0 表示的是最外层的事务 if ($this->_level == 0) { // 如果给定了隔离级别,那么就设定之 if ($isolationLevel !== null) { // 设定事务隔离级别 $this->db->getSchema()->setTransactionIsolationLevel($isolationLevel); } Yii::trace('Begin transaction' . ($isolationLevel ? ' with isolation level ' . $isolationLevel : ''), __METHOD__); $this->db->trigger(Connection::EVENT_BEGIN_TRANSACTION); $this->db->pdo->beginTransaction(); $this->_level = 1; return; } // 以下 _level>0 表示的是嵌套的事务 $schema = $this->db->getSchema(); // 要使用嵌套事务,前提是所使用的数据库要支持 if ($schema->supportsSavepoint()) { Yii::trace('Set savepoint ' . $this->_level, __METHOD__); // 使用事务保存点 $schema->createSavepoint('LEVEL' . $this->_level); } else { Yii::info('Transaction not started: nested transaction not supported', __METHOD__); } // 结合 _level == 0 分支中的 $this->_level = 1, // 可以得知,一旦调用这个方法, _level 就会自增1 $this->_level++; } ~~~ 对于最外层的事务,即当 _level 为 0 时,最终落到PDO的 beginTransaction() 来启用事务。在启用前,如果开发者给定了隔离级别,那么还需要设定隔离级别。 当 _level > 0 时,表示的是嵌套的事务,并非最外层的事务。 对此,Yii使用 SQL 的 SAVEPOINT 和ROLLBACK TO SAVEPOINT 来实现设置事务保存点和回滚到保存点的操作。 ## 嵌套事务[](http://www.digpage.com/transaction.html#id4 "Permalink to this headline") 在开头的例子中,展现的是事务最简单的使用形式。Yii还允许把事务嵌套起来使用。 比如,可以采用如下形式来使用事务: ~~~ $outerTransaction = $db->beginTransaction(); try { $db->createCommand($sql1)->execute(); $innerTransaction = $db->beginTransaction(); try { $db->createCommand($sql2)->execute(); $db->createCommand($sql3)->execute(); $innerTransaction->commit(); } catch (Exception $e) { $innerTransaction->rollBack(); } $db->createCommand($sql4)->execute(); $outerTransaction->commit(); } catch (Exception $e) { $outerTransaction->rollBack(); } ~~~ 为了实现这一嵌套,Yii使用 yii\db\Transaction::_level 来表示嵌套的层级。 当层级为 0 时,表示的是最外层的事务。 一般情况下,整个Yii应用使用了同一个数据库连接,或者说是使用了单例。 具体可以看 [_服务定位器(Service Locator)_](http://www.digpage.com/service_locator.html#service-locator) 部分。 而在 yii\db\Connection 中,又对事务对象进行了缓存: ~~~ class Connection extends Component { // 保存当前连接的有效Transaction对象 private $_transaction; // 已经缓存有事务对象,且事务对象有效,则返回该事务对象 // 否则返回null public function getTransaction() { return $this->_transaction && $this->_transaction->getIsActive() ? $this->_transaction : null; } // 看看启用事务时,是如何使用事务对象的 public function beginTransaction($isolationLevel = null) { $this->open(); // 缓存的事务对象有效,则使用缓存中的事务对象 // 否则创建一个新的事务对象 if (($transaction = $this->getTransaction()) === null) { $transaction = $this->_transaction = new Transaction(['db' => $this]); } $transaction->begin($isolationLevel); return $transaction; } } ~~~ 因此,可以认为整个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() 。 当然,为了使代码功能有一定的层次感,在必要时,也可以使用嵌套的事务。 但要考虑好,子事务是否真的要吞掉异常?有没有必要继续抛出异常, 使得上一层级的事务也产生回滚?这个要根据实际的情形来确定。 ## 提交和回滚[](http://www.digpage.com/transaction.html#id5 "Permalink to this headline") 提交和回滚通过 Transaction::commit() 和 Transaction::rollBack() 来实现: ~~~ public function commit() { if (!$this->getIsActive()) { throw new Exception('Failed to commit transaction: transaction was inactive.'); } // 与begin()对应,只要调用 commit(),_level 自减1 $this->_level--; // 如果回到了最外层事务,那么应当使用PDO的commit if ($this->_level == 0) { Yii::trace('Commit transaction', __METHOD__); $this->db->pdo->commit(); $this->db->trigger(Connection::EVENT_COMMIT_TRANSACTION); return; } // 以下是尚未回到最外层的情形 $schema = $this->db->getSchema(); if ($schema->supportsSavepoint()) { Yii::trace('Release savepoint ' . $this->_level, __METHOD__); // 释放那么保存点 $schema->releaseSavepoint('LEVEL' . $this->_level); } else { Yii::info('Transaction not committed: nested transaction not supported', __METHOD__); } } public function rollBack() { if (!$this->getIsActive()) { return; } // 调用 rollBack() 也会使 _level 自减1 $this->_level--; // 如果已经返回到最外层,那么调用 PDO 的 rollBack if ($this->_level == 0) { Yii::trace('Roll back transaction', __METHOD__); $this->db->pdo->rollBack(); $this->db->trigger(Connection::EVENT_ROLLBACK_TRANSACTION); return; } // 以下是未返回到最外层的情形 $schema = $this->db->getSchema(); if ($schema->supportsSavepoint()) { Yii::trace('Roll back to savepoint ' . $this->_level, __METHOD__); // 那么就回滚到保存点 $schema->rollBackSavepoint('LEVEL' . $this->_level); } else { Yii::info('Transaction not rolled back: nested transaction not supported', __METHOD__); throw new Exception('Roll back failed: nested transaction not supported.'); } } ~~~ 对于提交和回滚: * 提交时,会使层级+1,回滚时,会使层级-1 * 对于最外层的提交和回滚,使用的是数据库事务的 commit 和 rollBack * 对于嵌套的内层的提交和回滚,使用的其实是事务保存点的释放和回滚 * 释放保存点时,会释放保存点的标识符,这个标识符在下次事务嵌套达到这个层级时, 会被再次使用。 ## 有效的事务[](http://www.digpage.com/transaction.html#id6 "Permalink to this headline") 在上面的提交、回滚等方法的代码中,我们多次看到了一个 this->getIsActive() 。 这是用于判断当前事务是否有效的一个方法,我们通过它,来看看什么样的一个事务, 算是有效的: ~~~ public function getIsActive() { return $this->_level > 0 && $this->db && $this->db->isActive; } ~~~ 方法很简单明了,一个有效的事务必须同时满足3个条件: * _level > 0 。这是由于为0是,要么是刚刚初始化, 要么是所有的事务已经提交或回滚了。也就是说,只有调用过了 begin() 但还没有调用过匹配的 commit() 或 rollBack() 的事务对象,才是有效的。 * 数据库连接要已经初始化。 * 数据库连接也必须是有效的。 如果觉得《深入理解Yii2.0》对您有所帮助,也请[帮助《深入理解Yii2.0》](http://www.digpage.com/donate.html#donate)。 谢谢!
';