Laravel 之巢狀事務 transactions 實現

牛玉富發表於2018-08-09

寫在之前

關於mysql 的事務巢狀可以檢視這個地址:
https://dev.mysql.com/doc/refman/5.5/en/im...

裡面有這麼一句話。

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方法,只有在最外層時才會真正的提交。

總結:

  1. 基本實現原理是 savepoint
  2. 通過$this->transactions對應的數值設定 不同的savepoint實現不同層次巢狀
  3. 只有在最後一個commit時才會真正提交請求。

SAVEPOINT 使用demo如下:

mysql> CREATE TABLE T2(ID INT);
Query OK, 0 rows affected (0.05 sec)

mysql> select * from T2;
Empty set (0.17 sec)

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

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

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

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

mysql> ROLLBACK TO SAVEPOINT trans1;
Query OK, 0 rows affected (0.04 sec)

mysql> RELEASE SAVEPOINT trans1;
Query OK, 0 rows affected (0.03 sec)

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

mysql> SELECT * FROM T2;
+------+
| ID   |
+------+
|  100 |
+------+
1 row in set (0.05 sec)
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章