資料庫交易是確保轉帳、扣款、庫存扣減等關鍵業務資料一致性的核心機制。本文提供 MySQLi 程序式與 PDO 物件導向兩套完整實作,涵蓋自動提交控制、錯誤處理、回滾機制、死鎖重試等生產級細節。
<?php
header("Content-Type: text/html; charset=utf-8");
$mysqli = new mysqli("localhost", "root", "064319", "php");
$mysqli->set_charset("utf8mb4");
if ($mysqli->connect_errno) {
die('資料庫連線失敗: ' . $mysqli->connect_error);
}
try {
// 1. 關閉自動提交
$mysqli->autocommit(false);
$flag = true; // 交易成功旗標
// 2. 扣款:帳戶 3 扣除 1000
$sql = "UPDATE account SET balance = balance - 1000 WHERE id = 3";
$result = $mysqli->query($sql);
$affected = $mysqli->affected_rows;
if (!$result || $affected === 0) {
throw new Exception("扣款失敗:帳戶 3 不存在或餘額不足");
}
// 3. 入帳:帳戶 2 增加 1000
$sql = "UPDATE account SET balance = balance + 1000 WHERE id = 2";
$result = $mysqli->query($sql);
$affected = $mysqli->affected_rows;
if (!$result || $affected === 0) {
throw new Exception("入帳失敗:帳戶 2 不存在");
}
// 4. 記錄流水(可選)
$sql = "INSERT INTO transfer_log (from_id, to_id, amount, created_at) VALUES (3, 2, 1000, NOW())";
$result = $mysqli->query($sql);
if (!$result) {
throw new Exception("流水記錄失敗");
}
// 5. 全部成功,提交交易
$mysqli->commit();
echo '轉帳成功';
} catch (Exception $e) {
// 6. 任一失敗,回滾
$mysqli->rollback();
echo '轉帳失敗: ' . $e->getMessage();
// 記錄錯誤日誌
error_log("[TRANSFER FAILED] " . $e->getMessage());
} finally {
// 7. 恢復自動提交並關閉連線
$mysqli->autocommit(true);
$mysqli->close();
}
<?php
try {
// 1. 建立 PDO 連線
$pdo = new PDO(
"mysql:host=localhost;dbname=php;charset=utf8mb4",
"root",
"064319",
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 拋出例外
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, // 使用原生預處理
]
);
// 2. 關閉自動提交
$pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, 0);
// 3. 開始交易
$pdo->beginTransaction();
$amount = 1000;
// 4. 扣款(使用預處理語句防 SQL 注入)
$stmt = $pdo->prepare("UPDATE account SET balance = balance - ? WHERE id = ?");
$stmt->execute([$amount, 3]);
$affected1 = $stmt->rowCount();
if ($affected1 === 0) {
throw new PDOException("扣款失敗:帳戶 3 不存在或餘額不足");
}
// 5. 入帳
$stmt = $pdo->prepare("UPDATE account SET balance = balance + ? WHERE id = ?");
$stmt->execute([$amount, 2]);
$affected2 = $stmt->rowCount();
if ($affected2 === 0) {
throw new PDOException("入帳失敗:帳戶 2 不存在");
}
// 6. 記錄流水
$stmt = $pdo->prepare("INSERT INTO transfer_log (from_id, to_id, amount, created_at) VALUES (?, ?, ?, NOW())");
$stmt->execute([3, 2, $amount]);
// 7. 提交交易
$pdo->commit();
echo "操作成功";
} catch (PDOException $e) {
// 8. 回滾
if ($pdo->inTransaction()) {
$pdo->rollback();
}
echo "操作失敗: " . $e->getMessage();
error_log("[TRANSFER FAILED] " . $e->getMessage());
} finally {
// 9. 恢復自動提交
$pdo->setAttribute(PDO::ATTR_AUTOCOMMIT, 1);
$pdo = null; // 關閉連線
}
// 10. 驗證結果
echo "\n操作結果為:\n";
$sql = "SELECT * FROM account";
foreach ($pdo->query($sql) as $row) {
echo $row['id'] . " " . $row['balance'] . "\n";
}
| 維度 | MySQLi | PDO |
|---|---|---|
| API 風格 | 程序式 + 物件導向混合 | 純物件導向 |
| 資料庫支援 | 僅 MySQL/MariaDB | 12+ 種資料庫 |
| 預處理語句 | 支援 (prepare) |
支援 (原生/模擬可選) |
| 例外處理 | 需手動檢查回傳值 | ERRMODE_EXCEPTION 自動拋出 |
| 交易控制 | autocommit/commit/rollback |
beginTransaction/commit/rollback |
| 命名參數繫結 | 不支援 | 支援 (:name) |
| 多重查詢 | multi_query |
不支援 (安全性考量) |
function transferWithRetry(PDO $pdo, int $from, int $to, int $amount, int $maxRetries = 3) {
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
try {
$pdo->beginTransaction();
// 鎖定行(SELECT ... FOR UPDATE)防止並發問題
$stmt = $pdo->prepare("SELECT balance FROM account WHERE id IN (?, ?) FOR UPDATE");
$stmt->execute([$from, $to]);
$accounts = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
if (($accounts[$from] ?? 0) < $amount) {
throw new Exception("餘額不足");
}
$pdo->prepare("UPDATE account SET balance = balance - ? WHERE id = ?")->execute([$amount, $from]);
$pdo->prepare("UPDATE account SET balance = balance + ? WHERE id = ?")->execute([$amount, $to]);
$pdo->prepare("INSERT INTO transfer_log (from_id, to_id, amount) VALUES (?, ?, ?)")->execute([$from, $to, $amount]);
$pdo->commit();
return true;
} catch (PDOException $e) {
if ($pdo->inTransaction()) $pdo->rollback();
// 死鎖錯誤碼:MySQL 1213, SQLSTATE HY000
if ($e->getCode() === 'HY000' && $attempt < $maxRetries) {
usleep(rand(50000, 200000)); // 50-200ms 隨機延遲
continue;
}
throw $e;
}
}
return false;
}
-- 資料表加欄位 ALTER TABLE account ADD version INT DEFAULT 0; -- 更新時檢查版本 UPDATE account SET balance = balance - ?, version = version + 1 WHERE id = ? AND version = ?; -- PHP 檢查 affected_rows,為 0 表示衝突需重試
微服務架構下跨資料庫轉帳,採用補償交易:
// 步驟 1:凍結金額(建立待處理記錄) // 步驟 2:嘗試入帳目標帳戶 // 步驟 3:確認入帳成功 → 確認扣款 // 任一步驟失敗 → 執行補償操作(解凍、退款)
utf8mb4 字元集finally 區塊確保連線正確關閉FOR UPDATE