事務確保多條 SQL「要麼全成功、要麼全失敗」,是資料一致性基石。本文以 PDO 為例 (舊 mysql_* 函式已移除),示範標準事務流程、巢狀事務模擬、死鎖重試、唯讀事務優化,並列出常見誤區對照表。
<?php
$pdo = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
try {
$pdo->beginTransaction();
// 業務 SQL 1
$stmt = $pdo->prepare('INSERT INTO `orders` (`user_id`, `amount`, `status`) VALUES (?, ?, 0)');
$stmt->execute([$userId, $amount]);
$orderId = $pdo->lastInsertId();
// 業務 SQL 2 (扣減庫存)
$stmt = $pdo->prepare('UPDATE `products` SET `stock` = `stock` - 1 WHERE `id` = ? AND `stock` > 0');
$stmt->execute([$productId]);
if ($stmt->rowCount() === 0) {
throw new \Exception('庫存不足');
}
// 業務 SQL 3 (記錄流水)
$stmt = $pdo->prepare('INSERT INTO `order_logs` (`order_id`, `action`) VALUES (?, ?)');
$stmt->execute([$orderId, 'create']);
$pdo->commit();
echo "下單成功,訂單號: $orderId";
} catch (\Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
error_log('下單失敗: ' . $e->getMessage());
echo "下單失敗: " . $e->getMessage();
}
?>
| 規則 | 說明 |
|---|---|
| 表引擎必須 InnoDB | MyISAM 不支援事務,ALTER TABLE `t` ENGINE=InnoDB |
| 自動提交預設開啟 | PDO 建立連線即 autocommit=1,beginTransaction() 關閉它 |
| DDL 會隱性提交 | CREATE/ALTER/DROP TABLE、TRUNCATE 會強制 commit,勿放在事務中 |
| 事務不可巢狀 | MySQL 不支援巢狀事務,第二個 begin 會隱性 commit 第一個 (見下方模擬方案) |
| SELECT ... FOR UPDATE | 加排他鎖,防止並發修改同一行,需在事務中使用 |
| 死鎖自動回滾 | InnoDB 偵測到死鎖會自動回滾其中一個事務,應用層需捕獲 SQLSTATE[HY000]: General error: 1213 重試 |
function transactional(callable $callback, PDO $pdo) {
$savepoint = 'sp_' . uniqid('', true);
$pdo->exec("SAVEPOINT $savepoint");
try {
$result = $callback($pdo);
$pdo->exec("RELEASE SAVEPOINT $savepoint");
return $result;
} catch (\Throwable $e) {
$pdo->exec("ROLLBACK TO SAVEPOINT $savepoint");
throw $e;
}
}
// 使用
$pdo->beginTransaction();
try {
// 外層邏輯
transactional(function($pdo) {
// 內層邏輯,可獨立回滾不影響外層
}, $pdo);
$pdo->commit();
} catch (\Throwable $e) {
$pdo->rollBack();
}
?>
function withDeadlockRetry(callable $callback, int $maxRetries = 3, int $baseDelayMs = 50) {
for ($attempt = 0; ; $attempt++) {
try {
return $callback();
} catch (\PDOException $e) {
if ($e->getCode() === 'HY000' && str_contains($e->getMessage(), '1213')) {
if ($attempt >= $maxRetries) throw $e;
usleep($baseDelayMs * 1000 * (1 << $attempt)); // 指數退避
continue;
}
throw $e;
}
}
}
// 使用
withDeadlockRetry(function() use ($pdo) {
$pdo->beginTransaction();
// ... 業務 ...
$pdo->commit();
});
?>
$pdo->beginTransaction();
$pdo->exec('SET TRANSACTION READ ONLY'); // 或 START TRANSACTION READ ONLY
// 僅執行 SELECT,無鎖、無 undo log、可從備庫讀取
$pdo->commit();
?>
| 誤區 | 正確做法 |
|---|---|
用 mysql_query('START TRANSACTION') 字串拼接 | 用 $pdo->beginTransaction(),自動處理驅動層狀態 |
catch 後忘記 rollBack() | 永遠在 catch 中檢查 inTransaction() 並回滾 |
| 事務中混入 DDL (建表、加索引) | DDL 會隱性提交,拆分為遷移腳本與業務事務分開執行 |
| 長事務 (超過幾秒) | 拆分業務、異步處理、或用 Saga 模式補償 |
| 以為 SELECT 不需事務 | 需一致性快照時 (如報表、對帳) 必須用事務 + REPEATABLE READ |
MySQL 事務核心三步:beginTransaction() → 業務 SQL → commit() / rollBack()。生產環境必備:**InnoDB 引擎、PDO 例外模式、死鎖重試、Savepoint 模擬巢狀、長事務拆分**。掌握這套模式,即可構建可靠的資料一致性保障。