事務與鎖#

事務就是要保證一組資料庫操作,要么全部成功,要么全部失敗。理解事務的隔離性和鎖機制,是掌握 MySQL 並發控制的關鍵。

事務的 ACID 特性#

特性英文說明
原子性Atomicity事務中的操作要么全部成功,要么全部失敗
一致性Consistency事務執行前後,資料保持一致狀態
隔離性Isolation多個事務並發執行時,相互隔離
持久性Durability事務一旦提交,修改就永久保存

隔離級別#

SQL 標準定義了四種事務隔離級別:

隔離級別說明問題
讀未提交事務未提交的變更,其他事務可見脏讀、不可重複讀、幻讀
讀提交事務提交後,變更才對其他事務可見不可重複讀、幻讀
可重複讀事務執行期間,看到的資料始終一致幻讀(InnoDB 已解決)
串行化讀寫都加鎖,完全串行執行

隔離級別示例#

假設表 T 中有一行資料 c=1:

事務 A                        事務 B
─────────────────────────────────────────
開始事務
查詢 c (V1)
                              開始事務
                              將 c 改為 2
查詢 c (V2)
                              提交
查詢 c (V3)
提交
隔離級別V1V2V3
讀未提交222
讀提交112
可重複讀111
串行化112

Oracle 預設隔離級別是「讀提交」,MySQL 預設是「可重複讀」。從 Oracle 遷移到 MySQL 時要注意設置。

-- 查看當前隔離級別
SHOW VARIABLES LIKE 'transaction_isolation';

-- 設置隔離級別
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

MVCC 實現原理#

InnoDB 通過 MVCC(多版本並發控制) 實作事務隔離。

資料版本鏈#

每條記錄在更新時都會生成新版本,並記錄事務 ID:

flowchart LR
    subgraph VersionChain["資料版本鏈"]
        V1["當前版本<br/>k=22<br/>trx_id=102"] --> V2["undo log<br/>k=11<br/>trx_id=101"]
        V2 --> V3["undo log<br/>k=1<br/>trx_id=90"]
    end

    style V1 fill:#c8e6c9
    style V2 fill:#fff3e0
    style V3 fill:#e3f2fd

一致性視圖 (Read View)#

事務啟動時會創建一致性視圖,記錄當前活躍的事務 ID:

flowchart LR
    subgraph RV["一致性視圖 (Read View)"]
        direction LR
        A["已提交<br/>(可見)"] --- B["低水位"]
        B --- C["活躍事務數組"]
        C --- D["高水位"]
        D --- E["未來事務<br/>(不可見)"]
    end

    style A fill:#c8e6c9
    style C fill:#fff3e0
    style E fill:#ffcdd2

活躍事務數組中的版本不可見,不在數組中的版本可見

可見性規則:

版本狀態可見性
版本未提交不可見
版本已提交,但在視圖創建後提交不可見
版本已提交,且在視圖創建前提交可見
自己的更新始終可見

InnoDB 利用「所有資料都有多個版本」的特性,實作了「秒級創建快照」的能力。

當前讀 vs 一致性讀#

讀類型說明場景
一致性讀讀取符合視圖規則的版本普通 SELECT
當前讀讀取最新已提交版本UPDATE、DELETE、SELECT … FOR UPDATE
-- 一致性讀
SELECT * FROM t WHERE id = 1;

-- 當前讀
SELECT * FROM t WHERE id = 1 LOCK IN SHARE MODE;  -- 讀鎖
SELECT * FROM t WHERE id = 1 FOR UPDATE;          -- 寫鎖
UPDATE t SET k = k + 1 WHERE id = 1;              -- 隱式寫鎖

更新資料時都是先讀後寫,這個讀只能讀當前最新值(當前讀),否則會丟失其他事務的更新。

行鎖機制#

MySQL 的行鎖是在引擎層實作的,InnoDB 支援行鎖,MyISAM 不支援。

兩階段鎖協議#

在 InnoDB 事務中,行鎖是在需要的時候才加上的,但要等到事務結束時才釋放。

flowchart TD
    A["事務開始"] --> B["UPDATE ... WHERE id=1<br/>(加鎖 id=1)"]
    B --> C["UPDATE ... WHERE id=2<br/>(加鎖 id=2)"]
    C --> D["... 其他操作 ..."]
    D --> E["事務提交"]
    E --> F["釋放所有行鎖"]

    style A fill:#e3f2fd
    style E fill:#fff3e0
    style F fill:#c8e6c9

鎖順序優化#

根據兩階段鎖協議,把最可能造成鎖衝突的操作放在事務最後

案例:電影票交易系統

業務需求:顧客 A 在影院 B 購買電影票

操作清單:

  1. 從顧客 A 賬戶扣款
  2. 給影院 B 賬戶加款
  3. 記錄交易日誌

分析:多個顧客同時購票時,語句 2 會產生衝突(更新同一個影院賬戶)

優化:按照 3 → 1 → 2 的順序執行,影院賬戶的鎖定時間最短

BEGIN;
INSERT INTO transaction_log ...;  -- 無衝突
UPDATE customer SET balance = balance - 100 WHERE id = 'A';  -- 低衝突
UPDATE theater SET balance = balance + 100 WHERE id = 'B';   -- 高衝突,放最後
COMMIT;

死鎖與死鎖檢測#

什麼是死鎖#

sequenceDiagram
    participant A as 事務 A
    participant B as 事務 B

    A->>A: UPDATE ... WHERE id=1<br/>(持有 id=1 的鎖)
    B->>B: UPDATE ... WHERE id=2<br/>(持有 id=2 的鎖)

    A--xB: UPDATE ... WHERE id=2<br/>(等待 id=2 的鎖)
    B--xA: UPDATE ... WHERE id=1<br/>(等待 id=1 的鎖)

    Note over A,B: 死鎖!相互等待

死鎖處理策略#

策略參數默認值說明
超時等待innodb_lock_wait_timeout50s等待超時後放棄
主動檢測innodb_deadlock_detectON發現死鎖後主動回滾一個事務

超時時間不能設太短(如 1s),否則會把正常的鎖等待也誤殺。正常情況下應該使用主動死鎖檢測。

死鎖檢測的性能問題#

死鎖檢測的時間複雜度是 O(n),當 1000 個事務同時更新同一行時,檢測操作達到 100 萬量級。

表現:CPU 使用率很高,但每秒執行不了幾個事務。

解決熱點行更新的方法#

方法一:關閉死鎖檢測(不推薦)

  • 風險大,可能導致大量超時

方法二:控制並發度

  • 在中間件或資料庫伺服器端控制
  • 相同行的更新在進入引擎前排隊

方法三:拆分熱點行

原來:影院賬戶 1 行
拆分後:影院賬戶 10 行,總額 = sum(各行)

每次加款:隨機選一行更新
衝突概率:降為 1/10

如果賬戶可能減少(如退票),需要特殊處理某行變成 0 的情況。

長事務的危害#

為什麼要避免長事務#

  1. 占用回滾段:長事務需要保留所有可能用到的 undo log
  2. 占用鎖資源:長時間持有行鎖,影響並發
  3. 可能拖垮資料庫:資源不能及時釋放

MySQL 5.5 及之前版本,回滾日誌放在 ibdata 文件中,即使事務提交,文件也不會變小。

如何避免長事務#

應用端:

  1. 確認 autocommit=1,不要關閉自動提交
  2. 去掉不必要的只讀事務(多個 SELECT 不需要事務包裹)
  3. 設置 MAX_EXECUTION_TIME 限制語句執行時間

資料庫端:

  1. 監控 information_schema.innodb_trx
  2. 設置長事務閾值,超過就報警或 kill
  3. 使用 pt-kill 工具
-- 查找持續超過 60 秒的事務
SELECT * FROM information_schema.innodb_trx
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60;

事務啟動方式#

方式說明建議
BEGIN / START TRANSACTION顯式啟動推薦
SET autocommit=0關閉自動提交不推薦(容易導致長事務)
COMMIT WORK AND CHAIN提交並自動啟動下一個事務頻繁事務場景可用

本章小結#

主題要點
隔離級別讀未提交 < 讀提交 < 可重複讀 < 串行化
MVCC數據多版本 + 一致性視圖 = 秒級快照
當前讀UPDATE/DELETE/SELECT FOR UPDATE 讀最新版本
兩階段鎖事務結束才釋放鎖,衝突操作放最後
死鎖主動檢測 + 控制並發度 + 拆分熱點行
長事務占用回滾段和鎖資源,應該避免

關鍵組態:autocommit = 1(預設開啟自動提交)、innodb_deadlock_detect = ON(開啟死鎖檢測)、監控長事務並及時處理。