事務是數據庫併發控制與數據一致性的基石。MySQL 僅 InnoDB 支援事務。本文系統講解 ACID 原理、事務控制語句、鎖模式、隔離級別、死鎖處理及 PHP 封裝實戰。
CREATE TABLE t (...) ENGINE=InnoDBSHOW VARIABLES LIKE 'have_innodb'; → YES| 特性 | 含義 | MySQL 實現 |
|---|---|---|
| 原子性 | 全做或全不做 | Undo Log 回滾 |
| 一致性 | 約束不被破壞 | 外鍵、觸發器、應用邏輯 |
| 隔離性 | 併發事務互不干擾 | MVCC + 鎖(4 隔離級別) |
| 持久性 | 提交後永久生效 | Redo Log (WAL) + 雙寫緩衝 |
-- 開啟
START TRANSACTION; -- 或 BEGIN
-- 提交
COMMIT;
-- 回滾
ROLLBACK;
-- 保存點
SAVEPOINT sp1;
ROLLBACK TO SAVEPOINT sp1; -- 回滾到保存點
RELEASE SAVEPOINT sp1;
SET AUTOCOMMIT = 0; -- 關閉,後續語句同屬一事務,直到 COMMIT/ROLLBACK
SET AUTOCOMMIT = 1; -- 開啟(默認),每條語句隱式提交
| 模式 | SQL | 兼容性 | 場景 |
|---|---|---|---|
| 共享鎖 (S) | SELECT ... LOCK IN SHARE MODE | 兼容 S,阻塞 X | 讀取需防止被修改 |
| 排他鎖 (X) | SELECT ... FOR UPDATEUPDATE/DELETE/INSERT | 阻塞 S、X | 修改前確認狀態 |
| 意向鎖 (IS/IX) | 隱式加於表級 | — | 多粒度鎖協協調 |
-- 命中索引 → 行鎖
SELECT * FROM t WHERE uid = 'xxx' FOR UPDATE;
-- 未命中索引 → 表鎖(全表掃描加 X 鎖)
SELECT * FROM t WHERE unindexed_col = 'xxx' FOR UPDATE;
| 級別 | 臟讀 | 不可重複讀 | 幻讀 | 鎖策略 | 適用場景 |
|---|---|---|---|---|---|
| READ UNCOMMITTED | Y | Y | Y | 無 S 鎖 | 極少用 |
| READ COMMITTED | N | Y | Y | 行鎖(無間隙鎖) | 高併發、語句級一致性 |
| REPEATABLE READ (默認) | N | N | N* | Next-Key Lock | 一般業務、事務級一致性 |
| SERIALIZABLE | N | N | N | 表級鎖/全共享鎖 | 極高一致性要求 |
* RR 級別通過 Next-Key Lock(行鎖+間隙鎖)解決幻讀。
-- 下一事務生效
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 全局新連接生效
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
ERROR 1213 (HY000): Deadlock found 重試innodb_lock_wait_timeout (默認 50s) 控制等待超時<?php
function executeTransaction(mysqli $mysqli, array $sqls) {
$mysqli->autocommit(false);
try {
foreach ($sqls as $sql) {
if (!$mysqli->query($sql)) {
throw new Exception($mysqli->error);
}
}
$mysqli->commit();
return true;
} catch (Exception $e) {
$mysqli->rollback();
error_log('Transaction rolled back: ' . $e->getMessage());
return false;
} finally {
$mysqli->autocommit(true);
}
}
// 使用
$mysqli = new mysqli('localhost', 'user', 'pass', 'db');
$mysqli->set_charset('utf8mb4');
$ok = executeTransaction($mysqli, [
"UPDATE accounts SET balance = balance - 100 WHERE id = 1",
"UPDATE accounts SET balance = balance + 100 WHERE id = 2",
"INSERT INTO transfers (from_id, to_id, amount) VALUES (1, 2, 100)"
]);
$mysqli->close();
<?php
function pdoTransaction(PDO $pdo, callable $callback) {
$pdo->beginTransaction();
try {
$result = $callback($pdo);
$pdo->commit();
return $result;
} catch (Exception $e) {
$pdo->rollBack();
throw $e;
}
}
// 使用
$pdo = new PDO('mysql:host=localhost;dbname=db;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
pdoTransaction($pdo, function($pdo) {
$pdo->exec("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$pdo->exec("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
$pdo->prepare("INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)")
->execute([1, 2, 100]);
});
<?php
// 正確:SELECT FOR UPDATE 加排他鎖,防超賣
$pdo->beginTransaction();
$stmt = $pdo->prepare("SELECT stock FROM goods WHERE id = ? FOR UPDATE");
$stmt->execute([$goodsId]);
$stock = $stmt->fetchColumn();
if ($stock > 0) {
$pdo->prepare("UPDATE goods SET stock = stock - 1 WHERE id = ?")->execute([$goodsId]);
$pdo->prepare("INSERT INTO orders (goods_id, user_id) VALUES (?, ?)")->execute([$goodsId, $userId]);
$pdo->commit();
echo '下單成功';
} else {
$pdo->rollBack();
echo '庫存不足';
}
MySQL 事務核心是 InnoDB + ACID。實戰要點:選對隔離級別(默認 RR 通常足夠)、用好 SELECT FOR UPDATE 防併發、縮短事務範圍、統一加鎖順序防死鎖、PDO 封裝事務邏輯。金融、庫存、訂單等核心業務必須使用事務保證數據正確性。