事務四大特性 (ACID)
| 特性 | 說明 |
| Atomicity (原子性) | 全部成功或全部失敗,無中間狀態 |
| Consistency (一致性) | 資料從一個合法狀態變換到另一個合法狀態 |
| Isolation (隔離性) | 並發事務互不干擾 (READ COMMITTED 等級別) |
| Durability (持久性) | Commit 後寫入磁碟,斷電不丟失 |
前置條件
- 表引擎必須為 InnoDB (MyISAM 不支援事務)
- 單一連線內執行 (連線池需確保同一連線)
傳統 mysql_* 寫法 (已棄用,僅供維護舊專案參考)
<?php
$conn = mysql_connect('localhost', 'root', '');
mysql_select_db('test', $conn);
mysql_query("SET NAMES utf8");
mysql_query('START TRANSACTION');
$ok = true;
$ok &= mysql_query("INSERT INTO t1 (name) VALUES ('A')");
$ok &= mysql_query("INSERT INTO t2 (name) VALUES ('B')"); // 假設這裡失敗
if ($ok) {
mysql_query('COMMIT');
} else {
mysql_query('ROLLBACK');
}
mysql_close($conn);
現代 PDO 寫法 (推薦)
<?php
$dsn = 'mysql:host=localhost;dbname=test;charset=utf8mb4';
$pdo = new PDO($dsn, 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // 使用原生預處理
]);
try {
$pdo->beginTransaction();
$stmt = $pdo->prepare("INSERT INTO t1 (name) VALUES (?)");
$stmt->execute(['A']);
$stmt = $pdo->prepare("INSERT INTO t2 (name) VALUES (?)");
$stmt->execute(['B']); // 失敗會拋出 PDOException
$pdo->commit();
echo "事務提交成功";
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
echo "事務回滾: " . $e->getMessage();
}
關鍵陷阱與最佳實踐
1. 隱含提交
DDL 語句 (CREATE, ALTER, DROP, TRUNCATE) 會隱含提交當前事務。
- 事務中避免執行 DDL。
2. 巢狀事務模擬 (Savepoint)
MySQL 不支援真正巢狀事務,第二個 beginTransaction() 會隱含提交第一個。需用 Savepoint:
<?php
$pdo->beginTransaction(); // 外層
try {
$pdo->exec('SAVEPOINT sp1'); // 內層起點
$pdo->exec("INSERT INTO t3 VALUES ('C')");
// 內層邏輯失敗
throw new Exception('Inner fail');
$pdo->exec('RELEASE SAVEPOINT sp1'); // 內層成功
} catch (Throwable) {
$pdo->exec('ROLLBACK TO SAVEPOINT sp1'); // 僅回滾內層
}
// 外層繼續...
$pdo->commit();
3. 死鎖處理
<?php
$maxRetries = 3;
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
try {
$pdo->beginTransaction();
// ... 業務邏輯 ...
$pdo->commit();
break;
} catch (PDOException $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
if ($e->getCode() === 'HY000' && strpos($e->getMessage(), 'Deadlock') !== false) {
if ($attempt === $maxRetries) throw $e;
usleep(100000 * $attempt); // 指數退避
continue;
}
throw $e;
}
}
4. 長事務風險
- 鎖定資源久 → 並發下降
- Undo Log 膨脹
- 主從複寫延遲
- 原則:事務內只做必要的寫入,業務邏輯、外部 API 呼叫、檔案操作移出事務。
隔離等級與鎖
<?php
// 設定當前事務隔離等級
$pdo->exec("SET TRANSACTION ISOLATION LEVEL READ COMMITTED");
$pdo->beginTransaction();
// 或啟動時指定 (MySQL 8.0+)
$pdo->beginTransaction(PDO::TRANSACTION_READ_COMMITTED);
| 等級 | 髒讀 | 不可重複讀 | 幻讀 | 鎖行為 |
| READ UNCOMMITTED | 可能 | 可能 | 可能 | 無共享鎖 |
| READ COMMITTED (預設) | 否 | 可能 | 可能 | 讀取瞬間鎖 |
| REPEATABLE READ | 否 | 否 | 可能 (InnoDB 用 Next-Key Lock 解決) | 首次讀取鎖定範圍 |
| SERIALIZABLE | 否 | 否 | 否 | 全表鎖/範圍鎖 |
總結檢查清單
- 表引擎 = InnoDB
- 使用 PDO + Exception 模式
- 所有寫入用預處理
- 事務內只做 DB 寫入
- 捕獲例外時檢查
inTransaction() 再 rollBack()
- 死鎖重試機制
- 避免 DDL、避免長事務