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 的事務,導致原子性破壞。
核心邏輯:
_transactionNestingLevel 計數器追蹤嵌套深度。BEGIN TRANSACTION。SAVEPOINT nested_1),而非新事務。ROLLBACK;Level > 1 → ROLLBACK TO SAVEPOINT。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 鎖開銷;深層嵌套可能觸發鎖升級。
核心邏輯:
transactions 計數器。BEGIN TRANSACTION。ROLLBACK;Level > 1 → 計數器 -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 ✅) | 全相容 |
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; }
}