事務與鎖#
事務就是要保證一組資料庫操作,要么全部成功,要么全部失敗。理解事務的隔離性和鎖機制,是掌握 MySQL 並發控制的關鍵。
事務的 ACID 特性#
| 特性 | 英文 | 說明 |
|---|---|---|
| 原子性 | Atomicity | 事務中的操作要么全部成功,要么全部失敗 |
| 一致性 | Consistency | 事務執行前後,資料保持一致狀態 |
| 隔離性 | Isolation | 多個事務並發執行時,相互隔離 |
| 持久性 | Durability | 事務一旦提交,修改就永久保存 |
隔離級別#
SQL 標準定義了四種事務隔離級別:
| 隔離級別 | 說明 | 問題 |
|---|---|---|
| 讀未提交 | 事務未提交的變更,其他事務可見 | 脏讀、不可重複讀、幻讀 |
| 讀提交 | 事務提交後,變更才對其他事務可見 | 不可重複讀、幻讀 |
| 可重複讀 | 事務執行期間,看到的資料始終一致 | 幻讀(InnoDB 已解決) |
| 串行化 | 讀寫都加鎖,完全串行執行 | 無 |
隔離級別示例#
假設表 T 中有一行資料 c=1:
事務 A 事務 B
─────────────────────────────────────────
開始事務
查詢 c (V1)
開始事務
將 c 改為 2
查詢 c (V2)
提交
查詢 c (V3)
提交| 隔離級別 | V1 | V2 | V3 |
|---|---|---|---|
| 讀未提交 | 2 | 2 | 2 |
| 讀提交 | 1 | 1 | 2 |
| 可重複讀 | 1 | 1 | 1 |
| 串行化 | 1 | 1 | 2 |
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 購買電影票
操作清單:
- 從顧客 A 賬戶扣款
- 給影院 B 賬戶加款
- 記錄交易日誌
分析:多個顧客同時購票時,語句 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_timeout | 50s | 等待超時後放棄 |
| 主動檢測 | innodb_deadlock_detect | ON | 發現死鎖後主動回滾一個事務 |
超時時間不能設太短(如 1s),否則會把正常的鎖等待也誤殺。正常情況下應該使用主動死鎖檢測。
死鎖檢測的性能問題#
死鎖檢測的時間複雜度是 O(n),當 1000 個事務同時更新同一行時,檢測操作達到 100 萬量級。
表現:CPU 使用率很高,但每秒執行不了幾個事務。
解決熱點行更新的方法#
方法一:關閉死鎖檢測(不推薦)
- 風險大,可能導致大量超時
方法二:控制並發度
- 在中間件或資料庫伺服器端控制
- 相同行的更新在進入引擎前排隊
方法三:拆分熱點行
原來:影院賬戶 1 行
拆分後:影院賬戶 10 行,總額 = sum(各行)
每次加款:隨機選一行更新
衝突概率:降為 1/10如果賬戶可能減少(如退票),需要特殊處理某行變成 0 的情況。
長事務的危害#
為什麼要避免長事務#
- 占用回滾段:長事務需要保留所有可能用到的 undo log
- 占用鎖資源:長時間持有行鎖,影響並發
- 可能拖垮資料庫:資源不能及時釋放
MySQL 5.5 及之前版本,回滾日誌放在 ibdata 文件中,即使事務提交,文件也不會變小。
如何避免長事務#
應用端:
- 確認
autocommit=1,不要關閉自動提交 - 去掉不必要的只讀事務(多個 SELECT 不需要事務包裹)
- 設置
MAX_EXECUTION_TIME限制語句執行時間
資料庫端:
- 監控
information_schema.innodb_trx表 - 設置長事務閾值,超過就報警或 kill
- 使用
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(開啟死鎖檢測)、監控長事務並及時處理。