資料庫事務保證一組操作的原子性:要麼全成功,要麼全失敗回滾。本文基於 PDO + MySQL InnoDB,詳細講解事務的 ACID 特性、PHP 代碼實現模式、異常捕獲自動回滾、巢狀事務陷阱、以及生產環境最佳實踐清單。
| 特性 | 說明 |
|---|---|
| Atomicity (原子性) | 事務內所有操作視為不可分割單元,全做或全不做 |
| Consistency (一致性) | 事務前後資料庫從一個一致狀態變到另一個一致狀態 |
| Isolation (隔離性) | 並發事務互不干擾,MySQL 預設 REPEATABLE READ |
| Durability (持久性) | 提交後的修改永久保存,不因系統故障丟失 |
前提: 僅支援事務的儲存引擎 (InnoDB、BDB) 才生效。MyISAM 不支援 事務,操作會隱性提交。
setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // 拋出異常
$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
// 2. 關閉自動提交 (開啟事務模式)
$pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, 0);
// 等同於:$pdo->beginTransaction(); 但更明確
// 3. 開始事務
$pdo->beginTransaction();
try {
// --- 業務 SQL 操作區 ---
$amount = 100;
// 扣款
$stmt1 = $pdo->prepare("UPDATE accounts SET balance = balance - ? WHERE user_id = ? AND balance >= ?");
$stmt1->execute([$amount, 1, $amount]);
if ($stmt1->rowCount() === 0) {
throw new Exception("用戶 1 餘額不足");
}
// 入款
$stmt2 = $pdo->prepare("UPDATE accounts SET balance = balance + ? WHERE user_id = ?");
$stmt2->execute([$amount, 2]);
if ($stmt2->rowCount() === 0) {
throw new Exception("用戶 2 不存在");
}
// 記錄流水
$stmt3 = $pdo->prepare("INSERT INTO transactions (from_id, to_id, amount, created_at) VALUES (?, ?, ?, NOW())");
$stmt3->execute([1, 2, $amount]);
// --- 結束 ---
// 4. 全部成功,提交事務
$pdo->commit();
echo "轉帳成功";
} catch (Exception $e) {
// 5. 任一失敗,回滾事務
$pdo->rollBack();
echo "轉帳失敗: " . $e->getMessage();
}
// 6. 恢復自動提交模式
$pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, 1);
} catch (PDOException $e) {
die("資料庫連線失敗: " . $e->getMessage());
}
// 驗證結果
$stmt = $pdo->query("SELECT user_id, balance FROM accounts WHERE user_id IN (1,2)");
foreach ($stmt as $row) {
echo "User {$row['user_id']}: {$row['balance']}
";
}
?>
| 方法 | 說明 | 備註 |
|---|---|---|
| beginTransaction() | 啟動事務,關閉自動提交 | 返回 bool |
| commit() | 提交事務,恢復自動提交 | 成功返回 true |
| rollBack() | 回滾事務,恢復自動提交 | 成功返回 true |
| inTransaction() | 檢查是否在事務中 | PHP 5.3.3+ |
| setAttribute(ATTR_AUTOCOMMIT, 0/1) | 手動控制自動提交 | 較底層 |
$pdo->beginTransaction(); // 外層 // ... $pdo->beginTransaction(); // 內層 - 實際上不會開啟新事務! // ... $pdo->commit(); // 內層提交 - 實際提交了外層事務! // ... $pdo->rollBack(); // 外層回滾 - 此時已提交,回滾失效!
解決: 使用 inTransaction() 檢查,或封裝事務管理器類實現計數器模擬巢狀 (僅邏輯層面)。
CREATE TABLE、ALTER TABLE、TRUNCATE 等 DDL 會強制提交當前事務。事務中禁止混用 DDL。
innodb_lock_wait_timeout = 50 秒try {
$pdo->beginTransaction();
$pdo->exec("INSERT INTO users (email) VALUES ('a@b.com')"); // 成功
$pdo->exec("INSERT INTO users (email) VALUES ('a@b.com')"); // 唯一鍵衝突,拋出異常
$pdo->commit();
} catch (PDOException $e) {
$pdo->rollBack(); // 必須顯式回滾,否則第一條已隱性提交!
}
// 當前事務設定隔離等級
$pdo->exec("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED");
// 可選:READ UNCOMMITTED / READ COMMITTED / REPEATABLE READ / SERIALIZABLE
SHOW ENGINE INNODB STATUS 或監控系統告警 > 5 秒事務簡單場景 (如計數器增減) 可用單條 SQL 避免事務開銷:
-- 原子增減,無需事務 UPDATE accounts SET balance = balance - 100 WHERE user_id = 1 AND balance >= 100; -- 檢查 rowCount() 判斷成功與否
理解事務邊界、鎖行為、異常流向,配合 PDO 異常模式,即能編寫健壯、正確、高併發安全的資料庫操作代碼。