MySQL 原生不支援嵌套事務:
Transactions cannot be nested.
Implicit COMMIT occurs when START TRANSACTION is issued inside a transaction.
場景:Service A 開啟事務 → 調用 Service B (內部也開啟事務) → 導致外層事務被隱式提交,原子性破壞。
_transactionNestingLevel 計數嵌套層級。BEGIN TRANSACTION。SAVEPOINT (命名規則:DOCTRINE_SAVEPOINT_$level)。// beginTransaction()
++$this->_transactionNestingLevel;
if ($this->_transactionNestingLevel == 1) {
$this->_conn->beginTransaction();
} elseif ($this->_nestTransactionsWithSavepoints) {
$this->createSavepoint($this->_getNestedTransactionSavePointName());
}
// rollBack()
if ($this->_transactionNestingLevel == 1) {
$this->_conn->rollback();
$this->_transactionNestingLevel = 0;
} elseif ($this->_nestTransactionsWithSavepoints) {
$this->rollbackSavepoint($this->_getNestedTransactionSavePointName());
--$this->_transactionNestingLevel;
} else {
$this->_isRollbackOnly = true; // 標記外層必須回滾
--$this->_transactionNestingLevel;
}
// commit()
if ($this->_transactionNestingLevel == 1) {
$this->_conn->commit();
} elseif ($this->_nestTransactionsWithSavepoints) {
$this->releaseSavepoint($this->_getNestedTransactionSavePointName());
}
--$this->_transactionNestingLevel;
_nestTransactionsWithSavepoints 可關閉,退化為「僅標記回滾」模式。transactions 計數器,只有最外層真正操作數據庫事務。beginTransaction 僅計數 +1,不發送任何 SQL。rollBack 僅計數 -1,不真正回滾。commit/rollBack 才真正提交/回滾整個事務塊。// beginTransaction()
++$this->transactions;
if ($this->transactions == 1) {
$this->pdo->beginTransaction();
}
// rollBack()
if ($this->transactions == 1) {
$this->transactions = 0;
$this->pdo->rollBack();
} else {
--$this->transactions;
}
// commit()
if ($this->transactions == 1) {
$this->pdo->commit();
}
--$this->transactions;
| 維度 | Doctrine (Savepoint) | Laravel (Counter) |
|---|---|---|
| 嵌套語義 | 真嵌套,支持部分回滾 | 扁平化,整體提交/回滾 |
| 資源消耗 | 中 (Savepoint 元數據) | 極低 (純計數器) |
| 死鎖風險 | 較高 (長事務持有鎖) | 較高 (同樣長事務) |
| 適用場景 | 複雜業務樹、需獨立子事務回滾 | 簡單調用鏈、全鏈路強一致性 |
| 異常處理 | 靈活,可捕獲內層異常決定 | 剛性,內層異常必須拋出到外層 |
Doctrine 選擇「功能完備」,Laravel 選擇「簡單實用」。理解 Savepoint 與計數器的本質差異,結合業務一致性要求與架構複雜度,才能在 PHP 專案中正確落地嵌套事務模擬。