資料庫事務 是一組不可分割的操作序列,必須滿足 ACID 四大特性:
| 特性 | 英文 | 說明 | 轉帳範例 |
|---|---|---|---|
| 原子性 | Atomicity | 全做或全不做,不可部分執行 | 扣款與入帳必須同時成功 |
| 一致性 | Consistency | 事務前後資料庫狀態一致 | 總金額不變,不會憑空增減 |
| 隔離性 | Isolation | 並發事務互不干擾 | 轉帳過程中他人查詢見舊餘額 |
| 持久性 | Durability | 提交後永久保存,抗故障 | 系統崩潰後帳目不丟失 |
務必使用 InnoDB。MyISAM 不支援事務,僅適合純讀取/日誌類資料表。
CREATE TABLE accounts (
id INT PRIMARY KEY,
balance DECIMAL(12,2) NOT NULL DEFAULT 0
) ENGINE=InnoDB;
<?php
$mysqli = new mysqli('localhost', 'user', 'pass', 'bank_db');
if ($mysqli->connect_errno) {
die("連線失敗: " . $mysqli->connect_error);
}
$from = 1001; $to = 1002; $amount = 500.00;
try {
// 1. 關閉自動提交,開啟事務
$mysqli->autocommit(false);
// 或: $mysqli->begin_transaction(); // PHP 5.5+ 推薦
// 2. 扣款 (檢查餘額)
$stmt = $mysqli->prepare("UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?");
$stmt->bind_param('dii', $amount, $from, $amount);
$stmt->execute();
if ($stmt->affected_rows === 0) {
throw new Exception("餘額不足或帳戶不存在");
}
// 3. 入帳
$stmt = $mysqli->prepare("UPDATE accounts SET balance = balance + ? WHERE id = ?");
$stmt->bind_param('di', $amount, $to);
$stmt->execute();
if ($stmt->affected_rows === 0) {
throw new Exception("目標帳戶不存在");
}
// 4. 記錄流水 (可選)
$stmt = $mysqli->prepare("INSERT INTO transactions (from_id, to_id, amount, created_at) VALUES (?, ?, ?, NOW())");
$stmt->bind_param('iid', $from, $to, $amount);
$stmt->execute();
// 5. 全部成功,提交
$mysqli->commit();
echo "轉帳成功";
} catch (Exception $e) {
// 任何錯誤,回滾
$mysqli->rollback();
echo "轉帳失敗: " . $e->getMessage();
} finally {
// 恢復自動提交
$mysqli->autocommit(true);
$mysqli->close();
}
<?php
$pdo = new PDO('mysql:host=localhost;dbname=bank_db;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$from = 1001; $to = 1002; $amount = 500.00;
try {
$pdo->beginTransaction();
// 扣款
$stmt = $pdo->prepare("UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?");
$stmt->execute([$amount, $from, $amount]);
if ($stmt->rowCount() === 0) throw new Exception("餘額不足");
// 入帳
$stmt = $pdo->prepare("UPDATE accounts SET balance = balance + ? WHERE id = ?");
$stmt->execute([$amount, $to]);
if ($stmt->rowCount() === 0) throw new Exception("目標帳戶不存在");
// 流水
$stmt = $pdo->prepare("INSERT INTO transactions (from_id, to_id, amount) VALUES (?, ?, ?)");
$stmt->execute([$from, $to, $amount]);
$pdo->commit();
echo "轉帳成功";
} catch (Exception $e) {
$pdo->rollBack();
echo "轉帳失敗: " . $e->getMessage();
}
<?php
// 強烈不建議在新專案使用
$conn = mysql_connect('localhost', 'user', 'pass');
mysql_select_db('bank_db');
mysql_query('SET autocommit=0'); // 或 START TRANSACTION
mysql_query("UPDATE accounts SET balance = balance - 500 WHERE id = 1001");
mysql_query("UPDATE accounts SET balance = balance + 500 WHERE id = 1002");
if (mysql_errno()) {
mysql_query('ROLLBACK');
echo '失敗';
} else {
mysql_query('COMMIT');
echo '成功';
}
mysql_query('SET autocommit=1');
// 設定隔離等級 (需在 beginTransaction 前)
$pdo->exec("SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ");
// 常見等級:
// READ UNCOMMITTED - 髒讀、不可重複讀、幻讀
// READ COMMITTED - 不可重複讀、幻讀 (大多數 DB 預設)
// REPEATABLE READ - 幻讀 (InnoDB 預設)
// SERIALIZABLE - 完全串列化,效能最差
$pdo->beginTransaction();
$pdo->exec("SAVEPOINT sp1");
// ... 某些操作
if ($error) $pdo->exec("ROLLBACK TO SAVEPOINT sp1"); // 部分回滾
$pdo->commit();
掌握事務處理是後端開發核心技能,正確使用能保障資料一致性,避免嚴重的財務/業務邏輯錯誤。