存儲過程是封裝、管理數據庫事務的理想場所。本文系統講解 MySQL/InnoDB 事務支持架構、隔離級別、事務控制語句、Savepoint 應用、鎖與死鎖處理、樂觀/悲觀鎖策略及事務設計原則。
事務支持綁定於存儲引擎,而非 MySQL Server 層:
| 級別 | 別名 | 特徵 | 一致性/性能 |
|---|---|---|---|
| READ UNCOMMITTED | Dirty Read | 可讀未提交數據 | 最低隔離,性能最好,數據完整性風險大 |
| READ COMMITTED | 僅可讀已提交數據 | 語句級一致性讀 | |
| REPEATABLE READ | 事務內多次讀取結果一致 (InnoDB 默認) | 事務級一致性讀,默認推薦 | |
| SERIALIZABLE | 串行執行,讀加共享鎖 | 最高隔離,性能差,死鎖概率高 |
設置會話隔離級別:
SET TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE};
警告: 生產環境慎用 READ UNCOMMITTED (髒讀) 和 SERIALIZABLE (性能/死鎖)。
| 語句 | 作用 |
|---|---|
| START TRANSACTION / BEGIN | 顯式開啟事務,隱式提交當前事務 (如有) |
| COMMIT | 提交事務,持久化更改,釋放鎖 |
| ROLLBACK | 回滾事務所有更改,釋放鎖 |
| SAVEPOINT name | 創建保存點 |
| ROLLBACK TO SAVEPOINT name | 回滾到保存點,允許部分提交 |
| SET TRANSACTION ... | 設置隔離級別 |
| LOCK TABLES | 顯式表鎖,**隱式提交當前事務**,事務中避免使用 |
-- 方式 1: 關閉自動提交 SET autocommit = 0; UPDATE ...; UPDATE ...; COMMIT; -- 方式 2: 顯式 START TRANSACTION (推薦) START TRANSACTION; UPDATE ...; UPDATE ...; COMMIT;
關鍵差異: 若已在事務中,SET autocommit=0 無效;START TRANSACTION 會隱式提交舊事務並開啟新事務。
ALTER TABLE / CREATE / DROP / TRUNCATE / RENAME / LOCK TABLES / UNLOCK TABLES SET AUTOCOMMIT=1 / START TRANSACTION (嵌套時) -- 以上語句在事務中執行會隱式提交當前事務!
事務中盡量避免 DDL,或將 DDL 移至事務外。
場景:外層大事務中執行子操作,子操作失敗僅回滾子操作,外層繼續。
CREATE PROCEDURE nested_transfer (...)
BEGIN
DECLARE txn_error INT DEFAULT 0;
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SET txn_error = 1;
SAVEPOINT sp_transfer;
UPDATE account SET balance = balance - amt WHERE id = from_id;
IF txn_error THEN ROLLBACK TO sp_transfer; SELECT 'Aborted'; END IF;
UPDATE account SET balance = balance + amt WHERE id = to_id;
IF txn_error THEN ROLLBACK TO sp_transfer; SELECT 'Aborted'; END IF;
END;
注意: Savepoint 有性能開銷,簡單邏輯優先用 IF/ELSE 重寫避免。
COMMIT/ROLLBACK 釋放。SELECT ... FOR UPDATE; -- 排他鎖,阻塞其他讀/寫 SELECT ... LOCK IN SHARE MODE; -- 共享鎖,允許並發讀,阻塞寫
ERROR 1213 (40001): Deadlock found。-- 範例:轉賬前按 ID 排序加鎖,彻底避免死鎖
CREATE PROCEDURE safe_transfer (from_id INT, to_id INT, amt DECIMAL(10,2))
BEGIN
DECLARE lock_cursor CURSOR FOR
SELECT id FROM account WHERE id IN (from_id, to_id) ORDER BY id FOR UPDATE;
START TRANSACTION;
OPEN lock_cursor; FETCH lock_cursor INTO @dummy; -- 僅為加鎖
UPDATE account SET balance = balance - amt WHERE id = from_id;
UPDATE account SET balance = balance + amt WHERE id = to_id;
CLOSE lock_cursor;
COMMIT;
END;
innodb_lock_wait_timeout (默認 50s)。ERROR 1205 (HY000): Lock wait timeout exceeded。| 策略 | 原理 | 適用場景 | 實現關鍵 |
|---|---|---|---|
| 悲觀鎖 | 讀時加鎖 (FOR UPDATE),寫時持有鎖 | 衝突頻率高、等待可接受、強一致性要求 | SELECT ... FOR UPDATE 在事務開啟後立即執行 |
| 樂觀鎖 | 讀不加鎖,寫前驗證版本/時間戳未變 | 衝突極低、長業務驗證、可容忍重試 | 表加 version INT 或 ts TIMESTAMP,UPDATE ... SET ..., version=version+1 WHERE id=? AND version=old_ver |
悲觀鎖示例 (轉賬):
START TRANSACTION;
SELECT balance INTO @bal FROM account WHERE id=from_id FOR UPDATE;
IF @bal >= amt THEN
UPDATE account SET balance=balance-amt WHERE id=from_id;
UPDATE account SET balance=balance+amt WHERE id=to_id;
COMMIT;
ELSE ROLLBACK; END IF;
樂觀鎖示例 (長驗證場景):
SELECT ts, bal INTO @ts1, @bal FROM account WHERE id=from_id;
IF @bal >= amt THEN
CALL long_validation(from_id); -- 耗時驗證,不持有鎖
START TRANSACTION;
SELECT ts, bal INTO @ts2, @bal2 FROM account WHERE id=from_id FOR UPDATE;
IF @ts1=@ts2 AND @bal=@bal2 THEN
UPDATE account SET balance=balance-amt, ts=NOW() WHERE id=from_id;
UPDATE account SET balance=balance+amt WHERE id=to_id;
COMMIT;
ELSE ROLLBACK; END IF;
ELSE ... END IF;