交易確保一組相關的 SQL 操作要麼全部成功,要麼全部失敗回滾,維護資料一致性。經典場景:轉帳扣款與入帳必須同時成功,否則資料會不一致。
PDO::ATTR_ERRMODE_EXCEPTION)。$pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, 0);$pdo->beginTransaction();$pdo->commit();,失敗 $pdo->rollback();$pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, 1);<?php
// 資料庫連線設定
$dsn = "mysql:host=localhost;dbname=bank;charset=utf8mb4";
$user = "root";
$pass = "password";
try {
$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,
]);
} catch (PDOException $e) {
die("資料庫連線失敗:" . $e->getMessage());
}
/**
* 轉帳函式:從 $from_id 扣款 $amount 並存入 $to_id
* 回傳 true 成功,false 失敗
*/
function transfer($pdo, $from_id, $to_id, $amount) {
try {
// 1. 關閉自動提交
$pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, 0);
// 2. 開始交易
$pdo->beginTransaction();
// 3. 扣款(檢查餘額)
$stmt = $pdo->prepare("SELECT balance FROM accounts WHERE id = ? FOR UPDATE");
$stmt->execute([$from_id]);
$from = $stmt->fetch();
if (!$from || $from['balance'] < $amount) {
throw new Exception("餘額不足或帳戶不存在");
}
$pdo->prepare("UPDATE accounts SET balance = balance - ? WHERE id = ?")
->execute([$amount, $from_id]);
// 4. 入帳
$pdo->prepare("UPDATE accounts SET balance = balance + ? WHERE id = ?")
->execute([$amount, $to_id]);
// 5. 記錄交易流水
$pdo->prepare("INSERT INTO transactions (from_id, to_id, amount, created_at) VALUES (?, ?, ?, NOW())")
->execute([$from_id, $to_id, $amount]);
// 6. 全部成功,提交交易
$pdo->commit();
echo "轉帳成功!\n";
return true;
} catch (Exception $e) {
// 7. 發生任何錯誤,回滾
if ($pdo->inTransaction()) {
$pdo->rollback();
}
echo "轉帳失敗:" . $e->getMessage() . "\n";
return false;
} finally {
// 8. 還原自動提交模式
$pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, 1);
}
}
// 測試
transfer($pdo, 1, 2, 500); // 張三轉 500 給李四
?>
在交易中查詢餘額時加上 FOR UPDATE,會對該列加排他鎖,防止並發交易同時讀取舊餘額導致超扣(競態條件)。
PDOException、自訂 Exception、或 execute() 回傳 false 都應觸發回滾。finally 區塊確保自動提交模式一定會被還原,避免影響後續非交易查詢。PDO 不支援真正的巢狀交易。若在交易中呼叫另一個也開啟交易的函式,內層 commit() 實際上不會寫入,直到最外層 commit()。建議交易邏輯集中在單一函式,或使用 Savepoint(SAVEPOINT name / ROLLBACK TO SAVEPOINT name)實現部分回滾。
| 錯誤現象 | 原因 | 解決 |
|---|---|---|
| SQLSTATE[HY000]: General error: 2014 Cannot execute queries while other unbuffered queries are active | 同一連線有未讀完的結果集 | 確保前一個查詢的結果集已全部 fetch 或 closeCursor() |
| Deadlock found when trying to get lock; try restarting transaction | 多執行緒並發更新同筆資料順序不一致 | 統一更新順序(如按 id 排序)、縮短交易時間、重試機制 |
| Lock wait timeout exceeded | 等待鎖釋放超過 innodb_lock_wait_timeout(預設 50 秒) | 檢查長時間執行的交易、優化索引、調大 timeout |
$pdo->beginTransaction();
try {
$pdo->exec("INSERT INTO logs (msg) VALUES ('step 1')");
$pdo->exec("SAVEPOINT step1");
$pdo->exec("INSERT INTO logs (msg) VALUES ('step 2 - might fail')");
// 若這步失敗,只回滾到 step1
$pdo->exec("SAVEPOINT step2");
$pdo->exec("INSERT INTO logs (msg) VALUES ('step 3')");
$pdo->commit();
} catch (Exception $e) {
$pdo->exec("ROLLBACK TO SAVEPOINT step1"); // 保留 step1
$pdo->commit(); // 提交 step1
}
?>