事務具備 ACID 四大特性(原子性、一致性、隔離性、持久性)。PDO 為支援事務的資料庫提供完整介面,本文詳細說明事務配置、操作流程與最佳實踐。
// 1. 關閉自動提交(預設開啟) $pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, false); // 2. 開啟事務 $pdo->beginTransaction(); // 3. 提交或回滾 $pdo->commit(); // 全部成功 $pdo->rollback(); // 任一失敗
<?php
try {
$pdo->beginTransaction(); // 開啟事務
// 第一步操作
$row = $pdo->exec("INSERT INTO orders (user_id, total) VALUES (1, 100)");
if (!$row) throw new PDOException('訂單建立失敗');
$orderId = $pdo->lastInsertId();
// 第二步操作
$row = $pdo->exec("INSERT INTO order_items (order_id, product_id, qty) VALUES ($orderId, 5, 2)");
if (!$row) throw new PDOException('訂單明細建立失敗');
// 第三步操作:扣減庫存
$row = $pdo->exec("UPDATE products SET stock = stock - 2 WHERE id = 5");
if (!$row) throw new PDOException('庫存扣減失敗');
// 全部成功,提交事務
$pdo->commit();
echo "訂單處理成功";
} catch (PDOException $e) {
// 任一失敗,回滾事務
$pdo->rollback();
exit('事務失敗:' . $e->getMessage());
}
?>
| 項目 | 說明 |
|---|---|
| 資料庫引擎 | 必須使用 InnoDB,MyISAM 不支援事務 |
| 自動提交 | 事務期間必須關閉 ATTR_AUTOCOMMIT |
| DDL 語句 | CREATE/DROP/ALTER 會隱式提交,不可在事務中使用 |
| SELECT 鎖定 | 使用 SELECT ... FOR UPDATE 避免幻讀 |
| 巢狀事務 | PDO 不支援巢狀事務,需使用 Savepoint |
<?php
$pdo->beginTransaction();
try {
$pdo->exec("INSERT INTO a VALUES (1)");
// 建立儲存點
$pdo->exec("SAVEPOINT sp1");
try {
$pdo->exec("INSERT INTO b VALUES (2)");
// 子操作失敗時
throw new Exception("子操作失敗");
} catch (Exception $e) {
// 僅回滾到儲存點
$pdo->exec("ROLLBACK TO SAVEPOINT sp1");
echo "子操作已回滾,主事務繼續";
}
$pdo->exec("INSERT INTO c VALUES (3)");
$pdo->commit();
} catch (Exception $e) {
$pdo->rollback();
}
?>
// 設定隔離等級(需在 beginTransaction 前)
$pdo->exec("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED");
/* 隔離等級對照:
READ UNCOMMITTED - 讀未提交(髒讀)
READ COMMITTED - 讀已提交(不可重複讀)★ 預設
REPEATABLE READ - 可重複讀(幻讀)★ MySQL 預設
SERIALIZABLE - 序列化(效能最低)
*/
<?php
function transfer($pdo, $from, $to, $amount) {
try {
$pdo->beginTransaction();
// 檢查餘額(加鎖防併發)
$stmt = $pdo->prepare("SELECT balance FROM accounts WHERE id = ? FOR UPDATE");
$stmt->execute([$from]);
$balance = $stmt->fetchColumn();
if ($balance < $amount) {
throw new Exception("餘額不足");
}
// 扣款
$pdo->exec("UPDATE accounts SET balance = balance - $amount WHERE id = $from");
// 入帳
$pdo->exec("UPDATE accounts SET balance = balance + $amount WHERE id = $to");
// 記錄流水
$pdo->exec("INSERT INTO transactions (from_id, to_id, amount) VALUES ($from, $to, $amount)");
$pdo->commit();
return true;
} catch (Exception $e) {
$pdo->rollback();
error_log("轉帳失敗: " . $e->getMessage());
return false;
}
}
?>