事務ACID特性:數據庫的四大安全保障

想象你正在網購一雙運動鞋:下單時系統需要扣減庫存、創建訂單、扣減賬户餘額。如果在扣減庫存後突然斷電,會出現什麼情況?庫存減少了但訂單沒創建,這顯然是個災難!事務(Transaction) 就是為解決這類問題而生的數據庫核心機制。

事務必須滿足四大特性,業界簡稱ACID

原子性(Atomicity):要麼全做,要麼全不做

原子性確保事務中的所有操作是一個不可分割的整體。就像轉賬時,"A賬户扣款"和"B賬户收款"必須同時成功或同時失敗。
示例

複製

BEGIN; -- 開始事務
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- A賬户扣100
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- B賬户加100
COMMIT; -- 提交事務(全部生效)
-- 若中間出錯,執行 ROLLBACK; 回滾到初始狀態

如果第二條語句失敗,ROLLBACK會讓第一條語句的扣款也失效,保證數據一致性。

一致性(Consistency):從一個一致態到另一個一致態

事務執行前後,數據庫必須從一個合法狀態轉換到另一個合法狀態。例如轉賬業務中,"總金額不變"就是一種一致性約束。
場景:A有500元,B有300元,總金額800元。無論轉賬多少次,兩人餘額之和必須還是800元。

隔離性(Isolation):併發時互不干擾

當多個事務同時操作同一數據時,隔離性確保它們不會互相干擾。MySQL通過隔離級別控制干擾程度,這也是今天的重點內容。

持久性(Durability):一旦提交,永久保存

事務一旦提交,結果就會永久保存到數據庫,即使系統崩潰也不會丟失。MySQL通過redo log實現這一點——事務提交時,數據先寫入日誌,再異步刷到磁盤。

記憶口訣:A(原子)像炸彈要麼炸要麼不炸,C(一致)像天平永遠平衡,I(隔離)像隔音間互不打擾,D(持久)像刻在石頭上擦不掉。

隔離級別:控制併發的"隔音效果"

當多個用户同時操作數據庫時,可能出現髒讀、不可重複讀等問題。MySQL提供四種隔離級別,就像調節"隔音效果",級別越高隔離性越好,但性能開銷也越大。

1. 讀未提交(Read Uncommitted):最低隔離級別

一個事務可以讀取另一個未提交的事務數據。
問題:髒讀(Dirty Read)——讀取到未提交的"臨時數據"。
示例

  • 事務A:將商品價格從100元改為200元(未提交)
  • 事務B:讀取到價格200元
  • 事務A:因錯誤回滾,價格恢復為100元
  • 結果:事務B讀到了"不存在"的200元

2. 讀已提交(Read Committed):解決髒讀

一個事務只能讀取另一個已提交的事務數據。這是大多數數據庫的默認隔離級別(但MySQL不是)。
解決:髒讀問題
問題:不可重複讀(Non-repeatable Read)——同一事務內兩次讀取同一數據,結果不同。
示例

  • 事務A:第一次讀取商品價格為100元
  • 事務B:將價格改為200元並提交
  • 事務A:第二次讀取價格變為200元
  • 結果:事務A兩次讀取結果不一致

3. 可重複讀(Repeatable Read):MySQL默認隔離級別

確保同一事務內多次讀取同一數據的結果一致。
解決:不可重複讀問題
問題:幻讀(Phantom Read)——同一事務內,兩次查詢的結果集行數不同。
示例

  • 事務A:查詢庫存中所有紅色商品,得到10條記錄
  • 事務B:新增一條紅色商品並提交
  • 事務A:再次查詢,得到11條記錄
  • 結果:事務A像看到"幻覺"一樣多了一條記錄

4. 串行化(Serializable):最高隔離級別

所有事務串行執行,完全避免併發問題,但性能最差。
解決:所有併發問題
代價:相當於單線程執行,吞吐量極低,僅用於數據一致性要求極高的場景(如金融核心系統)。

隔離級別對比表

表格

複製

隔離級別

髒讀

不可重複讀

幻讀

性能

讀未提交

最高

讀已提交

可重複讀

串行化

如何設置隔離級別?

複製

-- 查看當前隔離級別
SELECT @@tx_isolation;

-- 設置全局隔離級別(需要重啓連接生效)
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- 設置當前會話隔離級別
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

注意:MySQL默認使用可重複讀,但通過MVCC(多版本併發控制) 機制,在該級別下也能避免幻讀,這是MySQL的特有優化。

併發問題:你可能遇到的"坑"

多個事務併發時,除了髒讀、不可重複讀、幻讀,還可能遇到這些問題:

1. 丟失更新(Lost Update)

兩個事務同時修改同一數據,後提交的事務覆蓋前者的修改。
示例

  • 商品庫存100件,事務A和B同時讀取到100
  • 事務A:下單減1,庫存99並提交
  • 事務B:下單減1,庫存99並提交
  • 結果:實際賣出2件,庫存只減了1件

2. 死鎖(Deadlock)

兩個事務互相等待對方釋放資源,導致永久阻塞。
示例

  • 事務A:鎖定商品A,等待鎖定商品B
  • 事務B:鎖定商品B,等待鎖定商品A
  • 結果:雙方無限等待,需數據庫自動檢測並終止其中一個事務

如何避免死鎖?

  • 固定加鎖順序(如總是先鎖ID小的記錄)
  • 控制事務大小,減少鎖定時間
  • 避免長事務

鎖機制:MySQL的"併發紅綠燈"

鎖是實現隔離級別的基礎,MySQL提供多種鎖類型,就像交通信號燈一樣控制數據訪問順序。

1. 按粒度分:表鎖 vs 行鎖

  • 表鎖:鎖定整個表,開銷小但併發低(如ALTER TABLE時自動加表鎖)。
  • 行鎖:只鎖定一行數據,開銷大但併發高(InnoDB支持,MyISAM不支持)。
    示例

複製

-- InnoDB行鎖(僅鎖定id=1的行)
  UPDATE products SET price=200 WHERE id=1;

2. 按功能分:共享鎖 vs 排他鎖

  • 共享鎖(S鎖):讀操作時加鎖,多個事務可同時持有。

複製

SELECT * FROM products WHERE id=1 LOCK IN SHARE MODE;

  • 排他鎖(X鎖):寫操作時加鎖,同一時間只允許一個事務持有。

複製

SELECT * FROM products WHERE id=1 FOR UPDATE; -- 寫鎖

兼容性規則:S鎖和S鎖兼容,S鎖和X鎖不兼容,X鎖和X鎖不兼容。就像"讀可以共享,寫必須獨佔"。

3. InnoDB特殊鎖:意向鎖

為了快速判斷表中是否有行鎖,InnoDB引入意向鎖:

  • 意向共享鎖(IS):事務準備加S鎖前,先加IS鎖
  • 意向排他鎖(IX):事務準備加X鎖前,先加IX鎖
    這些鎖由數據庫自動管理,用户無需手動操作。

4. 間隙鎖:解決幻讀的關鍵

在可重複讀級別下,InnoDB通過間隙鎖(Gap Lock) 防止幻讀。它鎖定的是一個範圍,而不僅僅是記錄本身。
示例

複製

-- 鎖定id在1-10之間的記錄(包括不存在的記錄)
SELECT * FROM products WHERE id BETWEEN 1 AND 10 FOR UPDATE;

這會阻止其他事務在該範圍內插入新記錄,從而避免幻讀。

實踐任務:動手驗證事務特性

任務1:體驗ACID特性

  1. 創建測試表:

複製

CREATE TABLE bank (
     id INT PRIMARY KEY,
     balance INT NOT NULL
   );
   INSERT INTO bank VALUES (1, 500), (2, 300);

  1. 模擬轉賬並故意出錯,觀察回滾效果:

複製

BEGIN;
   UPDATE bank SET balance = balance - 100 WHERE id = 1; -- A賬户扣100
   UPDATE bank SET balance = balance + 100 WHERE id = 2; -- B賬户加100
   -- 此時查詢:SELECT * FROM bank; 會看到A=400, B=400
   ROLLBACK; -- 回滾事務
   -- 再次查詢:A和B恢復為500和300,驗證原子性

任務2:測試不同隔離級別

  1. 打開兩個MySQL客户端窗口(會話A和會話B)
  2. 會話A設置隔離級別為讀未提交:

複製

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
   BEGIN;
   UPDATE bank SET balance = balance - 100 WHERE id = 1; -- 不提交

  1. 會話B查詢:

複製

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
   SELECT * FROM bank; -- 能看到A的餘額已減少(髒讀)

  1. 將會話B隔離級別改為讀已提交,重複步驟2-3,觀察是否還能看到未提交的修改。

任務3:模擬死鎖

  1. 會話A:鎖定id=1的行

複製

BEGIN;
   UPDATE bank SET balance=balance-100 WHERE id=1;

  1. 會話B:鎖定id=2的行

複製

BEGIN;
   UPDATE bank SET balance=balance-100 WHERE id=2;

  1. 會話A:嘗試鎖定id=2的行

複製

UPDATE bank SET balance=balance+100 WHERE id=2; -- 等待中

  1. 會話B:嘗試鎖定id=1的行

複製

UPDATE bank SET balance=balance+100 WHERE id=1; -- 觸發死鎖

此時MySQL會自動檢測死鎖並終止其中一個事務,觀察報錯信息。

總結:事務與併發控制的核心要點

  1. ACID是事務的四大支柱:原子性保證操作完整,一致性保證數據合法,隔離性控制併發干擾,持久性保證結果不丟失。
  2. 隔離級別是權衡的藝術:讀已提交解決髒讀,可重複讀解決不可重複讀,串行化解決所有問題但犧牲性能。
  3. 鎖是併發的控制器:行鎖粒度小併發高,表鎖粒度大開銷小;共享鎖用於讀,排他鎖用於寫。
  4. InnoDB的優勢:支持行鎖和事務,通過MVCC和間隙鎖優化併發性能,是MySQL推薦的存儲引擎。

思考問題:為什麼電商秒殺場景需要"超賣防護"?如何用今天學的事務和鎖機制實現?(答案明天揭曉)

下一天我們將學習存儲過程與函數,讓你的SQL代碼實現更復雜的業務邏輯!