Laravel 之嵌套事务 transactions 实现原理

关于 mysql 的事务嵌套可以查看这个地址:
https://dev.mysql.com/doc/refman/5.5/en/implicit-commit.html


里面有这么一句话。


Transactions cannot be nested. This is a consequence of the implicit commit performed for any current transaction when you issue a START TRANSACTION statement or one of its synonyms.


大体意思是 db 不支持事务嵌套,如果你嵌套执行 START TRANSACTION 时会隐式执行 commit


我们做个测试:


mysql>  BEGIN;
Query OK, 0 rows affected (0.16 sec)


mysql> INSERT INTO T2 VALUES(300);
Query OK, 1 row affected (0.29 sec)


mysql>  BEGIN;
Query OK, 0 rows affected (0.04 sec)


mysql> rollback;
Query OK, 0 rows affected (0.04 sec)


mysql> SELECT * FROM T2;
+------+
| ID   |
+------+
|  300 |
+------+
2 rows in set (0.04 sec)
果然,我们直接 rollback 上面的语句,但是还是执行了查询操作。


laravel 之嵌套事务 transactions 实现
为啥官网不支持,但是 laravel 框架却优雅的实现了事务嵌套,我们来看看它的实现原理。


调用示例:
\DB::beginTransaction();    //主事务
try{
    \DB::beginTransaction(); //子事务
    \DB::insert('insert into T2 set ID=100');
    \DB::rollBack();         //子事务回滚
    \DB::insert('insert into T2 set ID=200');
    \DB::commit();
}catch (\Exception $e) {
    \DB::rollBack();
    echo $e->getMessage();exit;
}
查看执行结果:


mysql> SELECT * FROM T2;
+------+
| ID   |
+------+
|  100 |
+------+
1 row in set (0.05 sec)
说明子事务成功回滚了,下面看下子事务的实现。


代码分析:
laravel/framework/src/Illuminate/Database/Concerns/ManagesTransactions.php 90 行


public function beginTransaction()
    {
        $this->createTransaction();


        $this->transactions++;


        $this->fireConnectionEvent('beganTransaction');
    }
每调一次 beginTransaction 会使 $this->transactions 加 1
接着看一下 $this->createTransaction (); 的实现


/**
     * Create a transaction within the database.
     *
     * @return void
     */
    protected function createTransaction()
    {
        if ($this->transactions == 0) {
            try {
                $this->getPdo()->beginTransaction();
            } catch (Exception $e) {
                $this->handleBeginTransactionException($e);
            }
        } elseif ($this->transactions >= 1 && $this->queryGrammar->supportsSavepoints()) {
            $this->createSavepoint();
        }
    }
if ($this->transactions == 0) 首先判断是否在事务中。


没有在事务中则执行 $this->getPdo()->beginTransaction()
相当于执行 BEGIN;


在事务中执行 $this->createSavepoint(); 下面是 createSavepoint 方法的实现。


/**
     * Create a save point within the database.
     *
     * @return void
     */
    protected function createSavepoint()
    {
        $this->getPdo()->exec(
            $this->queryGrammar->compileSavepoint('trans'.($this->transactions + 1))
        );
    }
这里相当于在 mysql 里执行 SAVEPOINT trans1;


下面看下 rollback 方法实现:


public function rollBack($toLevel = null)
{
    $toLevel = is_null($toLevel)
                ? $this->transactions - 1
                : $toLevel;


    if ($toLevel < 0 || $toLevel >= $this->transactions) {
        return;
    }
    $this->performRollBack($toLevel);
    $this->transactions = $toLevel;
    $this->fireConnectionEvent('rollingBack');
}
首先 rollback 会使 $this->transactions 减一。
然后调用 $this->performRollBack


protected function performRollBack($toLevel)
{
    if ($toLevel == 0) {
        $this->getPdo()->rollBack();
    } elseif ($this->queryGrammar->supportsSavepoints()) {
        $this->getPdo()->exec(
            $this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1))
        );
    }
}
performRollBack 方式实际就是在重新设定 savepoint 值。


下面看下 commit 的实现:


/**
 * Commit the active database transaction.
 *
 * @return void
 */
public function commit()
{
    if ($this->transactions == 1) {
        $this->getPdo()->commit();
    }


    $this->transactions = max(0, $this->transactions - 1);


    $this->fireConnectionEvent('committed');
}
commit 方法,只有在最外层时才会真正的提交。


总结:
基本实现原理是 savepoint
通过 $this->transactions 对应的数值设定 不同的 savepoint 实现不同层次嵌套
只有在最后一个 commit 时才会真正提交请求。

有话要说