本文是 MySQL overview 的 implementation-layer deep article。Overview 已說明 MySQL 在 OLTP 譜系的定位、本文聚焦 lock contention — 5 種 lock type + isolation level 互動 + production debug。


開場案例

Application 跑了 6 個月、staging 100% 重現過的 deadlock 從來沒在 production 出現。某天 traffic 上升 30%、production 開始爆 ER_LOCK_DEADLOCK、application retry 不夠快、order 大量失敗。

SHOW ENGINE INNODB STATUS\G 拉出 deadlock:

 1*** (1) TRANSACTION:
 2TRANSACTION 12345, ACTIVE 1 sec starting index read
 3mysql tables in use 1, locked 1
 4LOCK WAIT 4 lock struct(s), heap size 1136, 3 row lock(s)
 5MySQL thread id 100, query id 5000 update orders
 6UPDATE orders SET status = 'shipped' WHERE id = 500
 7
 8*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
 9RECORD LOCKS space id 50 page no 5 n bits 80 index PRIMARY of table `production`.`orders`
10trx id 12345 lock_mode X locks rec but not gap waiting
11
12*** (2) TRANSACTION:
13TRANSACTION 12346, ACTIVE 1 sec starting index read
14mysql tables in use 1, locked 1
154 lock struct(s), heap size 1136, 4 row lock(s)
16MySQL thread id 101, query id 5001 update payments
17UPDATE payments SET captured = 1 WHERE order_id = 500
18
19*** (2) HOLDS THE LOCK(S):
20RECORD LOCKS space id 50 page no 5 n bits 80 index PRIMARY of table `production`.`orders`
21trx id 12346 lock_mode X locks rec but not gap
22
23*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
24RECORD LOCKS space id 51 page no 10 n bits 80 index idx_order_id of table `production`.`payments`
25trx id 12346 lock_mode X waiting
26
27*** WE ROLL BACK TRANSACTION (1)

兩個 transaction 各自拿了一邊 lock、互相等對方的、deadlock。為什麼 staging 重現過、production 6 個月才爆?因為 lock contention 是 可能性 不是 確定性 — staging 重現等於確認「程式邏輯有 deadlock risk」、production 6 個月平安等於「concurrency 還沒撞到」。Traffic 上升把 機率乘以 N、原本每天 0 次變每分鐘 5 次。

這個 case 揭露 MySQL lock 教學的核心:理解 lock 不只是 debug 跑 deadlock 報錯 的能力、是 讀 query 預測 lock pattern 的能力。

InnoDB 5 種 Lock 類型

InnoDB 不是 簡單 row lock、有 5 個獨立 lock concept:

1. Record Lock — 鎖 row

SELECT ... FOR UPDATE / UPDATE / DELETE 對 被 match 的 row 加 record lock。

1-- Transaction 1
2BEGIN;
3SELECT * FROM orders WHERE id = 100 FOR UPDATE;
4-- 對 id=100 的 row 加 record lock

Transaction 2 試 UPDATE orders WHERE id = 100 必須等。

2. Gap Lock — 鎖 row 之間的「空隙」

InnoDB 在 REPEATABLE READ (預設) 下、SELECT ... FOR UPDATE WHERE col > 100 不只 lock 符合的 row、也 lock 該 range 內的「空隙」、防其他 transaction INSERT 進這個 range。

1-- 已存在 orders: id=100, 200, 300
2BEGIN;
3SELECT * FROM orders WHERE id > 100 AND id < 300 FOR UPDATE;
4-- Lock id=200 + gap lock (100, 200) + gap lock (200, 300)

Transaction 2 試 INSERT INTO orders (id) VALUES (150) 必須等 — 即使 id=150 不存在、gap lock 阻擋 INSERT。

Gap lock 是 deadlock 最常見來源 — application logic 看 row、但 lock 卻 cover row 之外的空隙、難預測。

3. Next-Key Lock — Record + Gap 組合

預設 lock 行為。SELECT ... FOR UPDATE WHERE col = 100 對 id=100 的 record lock + id=100 之前的 gap lock。

Lock 的範圍實際是 半開區間 (previous_id, current_id]:

1Records: 100, 200, 300
2
3WHERE id = 100 FOR UPDATE → next-key lock (-inf, 100]
4WHERE id = 200 FOR UPDATE → next-key lock (100, 200]
5WHERE id = 300 FOR UPDATE → next-key lock (200, 300]
6WHERE id BETWEEN 150 AND 250 FOR UPDATE → next-key lock (100, 200] + (200, 300]

4. Insert Intention Lock — INSERT 之前的 gap lock

INSERT 不直接 lock 整個 gap、而是 insert intention lock — 比 gap lock 弱、允許多個 INSERT 同 gap 並行(不同 id)。

1-- Transaction 1
2INSERT INTO orders (id) VALUES (150);
3-- Transaction 2
4INSERT INTO orders (id) VALUES (175);
5-- 同 gap (100, 200)、兩個 INSERT 並行、不阻塞

但如果 Transaction 1 已 hold gap lock(through SELECT FOR UPDATE)、Transaction 2 INSERT 必須等。

5. Auto-Inc Lock — Auto-Increment column 專用

INSERT INTO orders (id) VALUES (DEFAULT) 取得 auto-increment value 時 lock。Mode:

  • innodb_autoinc_lock_mode=0(traditional):lock 整個 INSERT statement 期間、其他 INSERT 必須等
  • innodb_autoinc_lock_mode=1(consecutive):lock 短時間(取值期間)、INSERT 1 row 不會阻塞其他
  • innodb_autoinc_lock_mode=2(interleaved、8.0+ 預設(5.7 預設仍是 1)):完全並行、auto-inc value 不保證連續但可並行

8.0+ 預設 mode=2、性能高、但 binlog format 必須 ROW(STATEMENT 行為錯)。

Isolation Level 對 Lock 的決定性影響

InnoDB 4 個 isolation level、lock 行為完全不同:

IsolationRead 行為Lock 範圍Default?
READ UNCOMMITTED可讀 dirty data純 record lock、無 gap
READ COMMITTED每個 statement 看當下 committed純 record lock、無 gap
REPEATABLE READTransaction 內 snapshot consistentRecord + gap + next-key
SERIALIZABLE強制 SELECT 變 SELECT … FOR SHARERecord + gap + next-key 加重

REPEATABLE READ + Gap lock 是 deadlock 主要來源

  • 預設 isolation level
  • 為了 保證 repeatable read(同 transaction 內讀同樣資料)、強制 gap lock 防 phantom row
  • 但 gap lock 經常 lock 比預期廣的範圍、deadlock 機率上升

改成 READ COMMITTED 的取捨

  • 優點:無 gap lock、deadlock 大降、寫吞吐上升
  • 缺點:transaction 內讀同 query 結果可能不同(non-repeatable read)
  • 重要:binlog format 必須 ROW(STATEMENT 在 READ COMMITTED 下 replication 行為不一致)
  • 多數 MySQL production 用 READ COMMITTED 跑 OLTP、REPEATABLE READ 留給特殊 case

對比 PostgreSQL

  • PG 預設 isolation 是 READ COMMITTED(不是 RR)
  • PG 的 RR 用 snapshot isolation(不靠 gap lock)、deadlock 少
  • 這是 MySQL 跟 PG 在 並行控制 model 的根本差異 — MySQL 用 lock-based、PG 用 MVCC-heavy

用 SHOW ENGINE INNODB STATUS 讀 lock 狀態

SHOW ENGINE INNODB STATUS\G 是 production debug lock contention 的主要工具:

 1------------
 2TRANSACTIONS
 3------------
 4Trx id counter 12350
 5Purge done for trx's n:o < 12340 undo n:o < 0 state: running but idle
 6History list length 5
 7
 8---TRANSACTION 12345, ACTIVE 30 sec  -- 長 transaction、警訊
 93 lock struct(s), heap size 1136, 5 row lock(s)
10MySQL thread id 100, OS thread handle ..., query id ...
11SELECT * FROM orders WHERE id > 100 FOR UPDATE
12------- TRX HAS BEEN WAITING 5 SEC FOR THIS LOCK:
13RECORD LOCKS space id 50 page no 5 n bits 80 index PRIMARY of table `production`.`orders`
14trx id 12345 lock_mode X locks gap before rec  -- gap lock

關鍵欄位:

  • ACTIVE N sec:transaction 跑多久(長 transaction 嫌疑)
  • lock_mode X / S:exclusive / shared lock
  • locks rec but not gap / locks gap before rec / locks rec:是 record / gap / next-key
  • TRX HAS BEEN WAITING N SEC FOR THIS LOCK:等多久、超過幾秒就是 lock contention

SELECT * FROM information_schema.INNODB_TRX / INNODB_LOCKS (5.7) / performance_schema.data_locks (8.0) 給 structured lock 視圖。

5 個 Production 踩雷

1. Gap lock 阻塞 INSERT — 「Lock 不存在的 row」

1-- Transaction 1
2BEGIN;
3SELECT * FROM orders WHERE user_id = 100 FOR UPDATE;
4-- 假設 user_id=100 沒任何 order、預期沒 lock 任何 row
5
6-- Transaction 2
7INSERT INTO orders (user_id, amount) VALUES (100, 50);
8-- 等!為什麼?

問題:WHERE user_id = 100 沒有 record 時、InnoDB 仍 lock user_id=100 應該在的 gap(防 phantom)、Transaction 2 INSERT 進這個 gap 被阻擋。

修法:

  • 改 READ COMMITTED isolation
  • 或不用 SELECT ... FOR UPDATE on empty result、改 application 層 check + INSERT pattern
  • INSERT ... ON DUPLICATE KEY UPDATEINSERT IGNORE 避免 SELECT FOR UPDATE

2. Auto-Inc Lock Contention — 大量並行 INSERT

innodb_autoinc_lock_mode=0=1 模式下、大量並行 INSERT 撞 auto-inc lock、寫吞吐 cap。

修法:

  • innodb_autoinc_lock_mode=2(interleaved、8.0+ 預設(5.7 預設仍是 1))
  • 確認 binlog_format=ROW(mode=2 必須)
  • 接受 auto-inc value 不連續(id 可能跳號)

3. FK Lock Cascading — 父子 transaction 互鎖

1-- orders 表有 customer_id FK → customers.id
2-- Transaction 1
3UPDATE customers SET name = '...' WHERE id = 100;  -- lock customers row
4
5-- Transaction 2
6INSERT INTO orders (customer_id, amount) VALUES (100, 50);
7-- FK check 需要 lock customers row id=100、等 Transaction 1

FK 強制 每個 INSERT child 都要 shared lock parent、parent 的任何 UPDATE 都會 lock 所有 child INSERT。

修法:

  • 評估 FK 是否真的需要(high-write 場景考慮 application-level enforcement)
  • 短 transaction 縮短 lock 時間
  • FK 設計時讓 parent UPDATE 少 / child INSERT 多(parent 是穩定資料)

4. Large Transaction Lock Holding — 1 個 transaction 拖全 cluster

1BEGIN;
2-- 100K row 的 batch UPDATE
3UPDATE orders SET status = 'archived' WHERE created_at < '2024-01-01';
4-- 跑 5 分鐘、持 100K row 的 lock
5-- 其他 transaction 撞到任何被 lock 的 row 都等 5 分鐘
6COMMIT;

長 transaction 是 lock contention 災難

修法:

  • 把 batch operation 拆 chunk(每 chunk 1000 row、commit、繼續):

    1DO {
    2  START TRANSACTION;
    3  UPDATE orders SET status = 'archived'
    4  WHERE created_at < '2024-01-01' AND status != 'archived'
    5  LIMIT 1000;
    6  COMMIT;
    7} WHILE rows_affected > 0;
  • pt-archiver tool(Percona)對 batch UPDATE / DELETE 自動 chunked

  • 監控 information_schema.innodb_trx 找出 long-running transaction

5. READ COMMITTED + Binlog ROW Interaction

READ COMMITTED isolation 改善 deadlock、但對 binlog format 有要求:

  • binlog_format=STATEMENT:READ COMMITTED 下 transaction 看到不同 snapshot、replicate 後 replica 結果可能 不同於 primary(broken replication semantically)
  • binlog_format=ROW:每個 row event 都 explicit、READ COMMITTED 跟 ROW 兼容、replica 結果一致
  • binlog_format=MIXED:部分 case 仍可能 fall back STATEMENT、不推薦

修法:

  • 用 READ COMMITTED 時、強制 binlog_format=ROW
  • 全 cluster server(primary + replica + Group Replication members)統一 binlog_format
  • Migration 5.7 STATEMENT → 8.0 ROW 時、isolation 跟 binlog format 一起 review

跟其他模組整合

跟 Replication

binlog_format=ROW 跟 isolation level 互動已述。Replica apply ROW binlog 時、replica 上 也 acquire 同樣 lock、replica 上的 long query 跟 replication lag 互動。詳見 Replication Topology

跟 Group Replication

GR certification phase 跟 row lock 衝突 — write conflict 檢測在 certification、不是 lock。但 local row lock 仍存在、影響 single-instance write throughput。詳見 Group Replication

跟 Online Schema Change

gh-ost / pt-osc 在 cut-over 階段需要 metadata lock、跟 long-running transaction 衝突。Lock contention deep dive 跟 OSC cut-over 議題密切。詳見 Online Schema Change Tools

跟 Query Optimization

Slow query 持 lock 久、放大 contention。EXPLAIN ANALYZE 看實際執行時間、跟 lock holding time 直接相關。詳見 Query Optimization

跟 InnoDB Tuning

innodb_lock_wait_timeout=50(預設 50 秒)— lock wait 超時 transaction 自動 rollback、避免無限等。production 建議調短(10-20 秒)、快 fail 給 application retry。詳見 InnoDB Tuning

跟 PostgreSQL Lock model 對比

維度MySQL InnoDBPostgreSQL
Concurrency modelLock-based(rec / gap / next-key)MVCC-heavy(few explicit lock)
預設 isolationREPEATABLE READREAD COMMITTED
Gap lock無對應(PG 用 predicate lock for SERIALIZABLE)
Deadlock 機率中-高
Auto-inc內建 + auto-inc lockSEQUENCE(無對應 lock 議題)
Snapshot isolation部分(RR 內)完整(MVCC 跑全 stack)

PG 用 MVCC 跑大部分並行 control、少數 case 才用 explicit lock、整體 deadlock 機率低。MySQL 用 lock-based + MVCC mixed、production 必須懂 lock pattern。

觀測 metric

Production 持續 monitor:

  • Innodb_row_lock_waits / _time → lock wait 累計
  • Innodb_deadlocks → deadlock 次數(5.7+ 有、之前要 parse SHOW ENGINE)
  • performance_schema.data_lock_waits → 即時 lock wait 視圖(8.0+)
  • information_schema.innodb_trx → long-running transaction
  • slow_query_log → 看 query 是否花太多 time 在 lock wait

對 deadlock:把 innodb_print_all_deadlocks=ON、所有 deadlock 寫 error log、不用 SHOW ENGINE 才看到。

何時改 isolation level

場景建議 isolation
典型 web OLTP、低-中寫吞吐REPEATABLE READ(預設)
高寫吞吐、deadlock 頻繁READ COMMITTED
金融 transaction、需要 strict isolationREPEATABLE READ + 仔細 review
嚴格 serializable(小 case)SERIALIZABLE(performance penalty)
跨 region replication + 強一致用 Group Replication / Spanner 而不是 isolation level

相關連結