首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 數據庫知識 >  >> MySQL 資料庫 >> MySQL 綜合教學

PHP 實現 MySQL 嵌套事務兩大方案:Doctrine 計數器+Savepoint vs Laravel 計數器+外層提交

     編輯:MySQL 綜合教學

PHP 實現 MySQL 嵌套事務兩大方案:Doctrine 計數器+Savepoint vs Laravel 計數器+外層提交

MySQL 原生不支援嵌套事務:START TRANSACTION 會隱式提交當前事務。ORM 框架透過計數器模式在應用層模擬嵌套語義,本文深度解析 Doctrine 與 Laravel 兩種實現策略。

問題根源

-- MySQL 官方文檔
Transactions cannot be nested. This is a consequence of the implicit commit
performed for any current transaction when you issue a START TRANSACTION statement.

場景:Service A 開啟事務 → 呼叫 Service B (也開啟事務) → B 完成 → A 完成。若直接用原生事務,B 的 START TRANSACTION 會提交 A 的事務,導致原子性破壞。

方案一:Doctrine — 計數器 + Savepoint (真嵌套)

核心邏輯:

  • _transactionNestingLevel 計數器追蹤嵌套深度。
  • Level 1:真正 BEGIN TRANSACTION。
  • Level > 1:建立 Savepoint (SAVEPOINT nested_1),而非新事務。
  • Rollback:Level 1 → ROLLBACK;Level > 1 → ROLLBACK TO SAVEPOINT。
  • Commit:Level 1 → COMMIT;Level > 1 → RELEASE SAVEPOINT。

原始碼精簡版:

public function beginTransaction() {
    ++$this->_transactionNestingLevel;
    if ($this->_transactionNestingLevel == 1) {
        $this->_conn->beginTransaction();  // 真正 BEGIN
    } elseif ($this->_nestTransactionsWithSavepoints) {
        $this->createSavepoint($this->_getNestedTransactionSavePointName());  // SAVEPOINT
    }
}

public function rollBack() {
    if ($this->_transactionNestingLevel == 1) {
        $this->_transactionNestingLevel = 0;
        $this->_conn->rollback();
    } elseif ($this->_nestTransactionsWithSavepoints) {
        $this->rollbackSavepoint($this->_getNestedTransactionSavePointName());
        --$this->_transactionNestingLevel;
    } else {
        $this->_isRollbackOnly = true;  // 標記外層必須回滾
        --$this->_transactionNestingLevel;
    }
}

public function commit() {
    if ($this->_transactionNestingLevel == 1) {
        $this->_conn->commit();
    } elseif ($this->_nestTransactionsWithSavepoints) {
        $this->releaseSavepoint($this->_getNestedTransactionSavePointName());
    }
    --$this->_transactionNestingLevel;
}

優點:真正支援部分回滾 (內層失敗回滾到 Savepoint,外層可繼續)。
缺點:Savepoint 增加 metadata 鎖開銷;深層嵌套可能觸發鎖升級。

方案二:Laravel — 計數器 + 單一外層事務 (偽嵌套)

核心邏輯:

  • transactions 計數器。
  • Level 1:BEGIN TRANSACTION。
  • Level > 1:什麼都不做 (計數器 +1)。
  • Rollback:Level 1 → ROLLBACK;Level > 1 → 計數器 -1,標記整體必須回滾。
  • Commit:Level 1 → COMMIT;Level > 1 → 計數器 -1。

原始碼精簡版:

public function beginTransaction() {
    ++$this->transactions;
    if ($this->transactions == 1) {
        $this->pdo->beginTransaction();
    }
}

public function rollBack() {
    if ($this->transactions == 1) {
        $this->transactions = 0;
        $this->pdo->rollBack();
    } else {
        --$this->transactions;
    }
}

public function commit() {
    if ($this->transactions == 1) {
        $this->pdo->commit();
    }
    --$this->transactions;
}

優點:極簡、零額外鎖開銷、效能最佳。
缺點:無法部分回滾,任一層 Rollback 導致整個外層事務回滾;內層「事務」實際不存在。

兩方案對比決策表

維度Doctrine (Savepoint)Laravel (計數器)
部分回滾能力✅ 支援❌ 不支援 (全回滾)
實現複雜度中 (Savepoint 管理)極低 (純計數器)
效能開銷額外 metadata 鎖幾乎零
適用場景需精細控制回滾邊界的複雜業務簡單外層原子性保護、高併發寫入
相容儲存引擎需支援 Savepoint (InnoDB ✅)全相容

自建 PDO 封裝參考實現 (綜合兩者優點)

class NestedTransaction {
    private $pdo;
    private $level = 0;
    private $savepointNames = [];
    private $mustRollback = false;

    public function __construct(PDO $pdo) { $this->pdo = $pdo; }

    public function begin() {
        if (++$this->level === 1) {
            $this->pdo->beginTransaction();
        } else {
            $sp = "sp_level_{$this->level}";
            $this->pdo->exec("SAVEPOINT {$sp}");
            $this->savepointNames[] = $sp;
        }
    }

    public function rollBack() {
        if ($this->level === 0) throw new \RuntimeException('No active transaction');
        if ($this->level === 1) {
            $this->pdo->rollBack();
            $this->level = 0;
            $this->mustRollback = false;
        } else {
            $sp = array_pop($this->savepointNames);
            $this->pdo->exec("ROLLBACK TO SAVEPOINT {$sp}");
            $this->pdo->exec("RELEASE SAVEPOINT {$sp}");
            --$this->level;
        }
    }

    public function commit() {
        if ($this->level === 0) throw new \RuntimeException('No active transaction');
        if ($this->mustRollback) throw new \RuntimeException('Transaction marked for rollback');
        if ($this->level === 1) {
            $this->pdo->commit();
            $this->level = 0;
        } else {
            $sp = array_pop($this->savepointNames);
            $this->pdo->exec("RELEASE SAVEPOINT {$sp}");
            --$this->level;
        }
    }

    public function markRollbackOnly() { $this->mustRollback = true; }
}

最佳實務建議

  1. 優先使用成熟 ORM (Doctrine/Laravel/Eloquent) 內建事務管理,勿手寫。
  2. 若需部分回滾 → 採用 Savepoint 方案 (Doctrine 風格)。
  3. 若僅需外層原子性、極致效能 → 採用計數器方案 (Laravel 風格)。
  4. 避免超過 3 層嵌套,重構為扁平服務調用 + 明確事務邊界。
  5. 長事務、大事務嚴禁嵌套,易導致鎖爭用、主從延遲、回滾耗時。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved