在高并发场景下,MVCC 实现:
能力 | 描述 |
---|---|
读写并发 | 读不会阻塞写,写也不会阻塞读(大多数 SELECT 是非阻塞的) |
一致性视图 | 每个事务看到的数据是其开始时的一致状态 |
高性能 | 避免频繁加锁,减少锁竞争开销 |
InnoDB 使用以下两部分实现 MVCC:
Undo Log(撤销日志):用于保存数据变更前的旧版本
ReadView(读视图):用于判断当前事务“能不能看到”某一行的某个版本
每当事务修改一条记录,InnoDB 会把修改前的值记录在 Undo Log 中:
UPDATE user SET age = 30 WHERE id = 1;
-- Undo Log: 保存 age = 20 的旧值
Undo Log 是行级的、链式结构,连接着该记录的所有历史版本,形成“版本链”。
每条数据记录会在内部隐藏字段中存储:
字段 | 作用 |
---|---|
DB_TRX_ID |
最后修改该记录的事务 ID(说白了,当前版本属于哪个事务) |
DB_ROLL_PTR |
回指 undo log 的指针,可沿指针“找老版本” |
DB_ROW_ID (隐藏主键) |
若无主键则生成的自增 ID |
示例:记录的多版本链
记录原始状态:age = 20
UPDATE age = 30 => Undo1: age = 20, trx_id=100
UPDATE age = 40 => Undo2: age = 30, trx_id=101
Undo1: age = 20, trx_id=100
记录链如下:
最新版本
┌──────────────┐
│ age = 40 │ trx_id = 102
│ roll_ptr → Undo2
└──────────────┘
↓
Undo2: age=30, trx_id=101
↓
Undo1: age=20, trx_id=100
当一个事务开始读取数据,InnoDB 会创建一个 ReadView(快照),它决定“哪些版本是我能看到的”。
字段 | 含义 |
---|---|
m_ids |
当前活跃事务列表(未提交事务的 trx_id) |
min_trx_id |
活跃事务中最小的事务 ID |
creator_trx_id |
当前事务的 ID |
up_limit_id |
下限:min(m_ids) |
low_limit_id |
上限:最大事务 ID(+1) |
当事务 A 在 ReadView 中读取某条记录,InnoDB 按以下条件判断该记录版本是否可见:
设 X
是记录的 DB_TRX_ID
,表示这条记录是哪个事务写的:
条件 | 是否可见 | 含义 |
---|---|---|
X == 当前事务 ID | ✅ 可见 | 当前事务自己写的数据 |
X < min_trx_id |
✅ 可见 | 提交时间早于当前快照 |
X ∈ 活跃事务列表 m_ids |
❌ 不可见 | 还未提交的事务,不能看到 |
X > low_limit_id |
❌ 不可见 | 新事务,还没提交,不能看到 |
若不满足可见性,InnoDB 会沿着
roll_ptr
追溯旧版本,直到找到符合 ReadView 的版本为止。
-- 假设当前数据:
id = 1, age = 20, trx_id = 100
-- 事务A:
START TRANSACTION; -- trx_id = 101
SELECT age FROM user WHERE id = 1; -- ReadView 创建
-- 事务B:
START TRANSACTION; -- trx_id = 102
UPDATE user SET age = 30 WHERE id = 1;
COMMIT;
-- 事务A再次读取:
SELECT age FROM user WHERE id = 1;
✅ 事务A两次读取都看到 age = 20,因为 ReadView 不包含事务102提交后的版本。
限制项 | 描述 |
---|---|
只适用于 SELECT | DELETE , UPDATE , INSERT 仍然加锁 |
仅限 RC/RR 隔离级别 | READ UNCOMMITTED 不使用 MVCC,SERIALIZABLE 强制加锁 |
范围加锁仍需要间隙锁 | 并不能防止幻读,仍需 SELECT ... FOR UPDATE 配合 |
Undo 不是永久保存 | 被 purge 线程清理后,不可回溯旧版本 |
事务A 事务B
│ │
┌────────────▼────────────────────▼────────────┐
│ InnoDB 引擎层 │
│ │
│ +-------------------------------+ │
│ | 当前记录(最新版本) | │
│ | DB_TRX_ID = 102 | ← 写入 │
│ | DB_ROLL_PTR → Undo2 | │
│ +-------------------------------+ │
│ ↓ │
│ Undo2(trx_id=101) ← A 可见 │
│ ↓ │
│ Undo1(trx_id=100) │
└──────────────────────────────────────────────┘
A 使用 ReadView(trx_id=101),只看见 trx_id ≤ 100 的版本
-- 查看当前事务
SELECT * FROM information_schema.innodb_trx;
-- 查看 undo 使用情况(8.0+)
SELECT * FROM performance_schema.data_locks;
组件 | 作用 |
---|---|
Undo Log | 存储旧版本,用于回滚与版本回溯 |
ReadView | 事务视图,决定“你能看到谁的版本” |
DB_TRX_ID | 表示记录是哪个事务写入的 |
DB_ROLL_PTR | 指向 undo,构成多版本链 |
可见性判断 | 是否在 ReadView 中“可见” = 是否为可读历史版本 |
场景 | 建议做法 |
---|---|
高并发下的数据一致性需求 | 开启 REPEATABLE READ ,使用 MVCC |
需要防止幻读 | 搭配 SELECT ... FOR UPDATE 使用 |
查询长时间读旧数据 | 合理配置 purge 线程参数,保留 undo |
手动事务控制 | 显式 START TRANSACTION 控制 ReadView 生命周期 |
在多事务并发执行中:
同一条记录可能被多个事务读写;
每个事务只能看到符合自己视图的数据版本(避免读未提交/幻读等);
InnoDB 使用 MVCC + 可见性算法 决定你是否能“看见”一条记录的某个版本。
这个过程的核心就是:判断某个版本(记录的 trx_id)是否对当前事务可见?
每条记录的当前版本都带有一个隐藏字段:
DB_TRX_ID
:最后修改该记录的事务 ID(也就是该版本由哪个事务创建)
DB_ROLL_PTR
:指向 undo log 的指针,用于回溯历史版本
每个事务读取数据时,会使用当前事务生成的 ReadView 来判断版本可见性。
设:
TRX_ID
:这条记录的版本是由哪个事务创建的;
trx_id
:当前事务的 ID;
ReadView.m_ids
:当前 ReadView 中的“活跃事务列表”(未提交);
up_limit_id
= 最小活跃事务 ID;
low_limit_id
= 当前系统中尚未分配的最小可用事务 ID;
条件 | 是否可见 | 含义 |
---|---|---|
TRX_ID == trx_id |
✅ 是 | 当前事务自己创建的版本(最新) |
TRX_ID < up_limit_id |
✅ 是 | 创建者事务早于当前快照创建,且已提交 |
TRX_ID ∈ ReadView.m_ids |
❌ 否 | 创建者事务未提交,不可见 |
TRX_ID ≥ low_limit_id |
❌ 否 | 创建者事务尚未开始(并发之后的事务),不可见 |
ReadView 是一个结构体,它描述了某个时刻所有活跃事务的快照,由 InnoDB 在以下场景中构建:
第一次执行 SELECT
(非锁定读取)时自动创建;
若显式开启事务:START TRANSACTION
+ 第一次查询时构建;
若自动提交模式:每条 SELECT
都独立构建一个新的 ReadView。
字段 | 含义 |
---|---|
creator_trx_id |
当前事务 ID |
up_limit_id |
活跃事务中最小的事务 ID |
low_limit_id |
当前系统中尚未分配的最小事务 ID(即最大已分配+1) |
m_ids[] |
当前所有活跃事务的 ID 列表(未提交的) |
这个结构决定了“哪些事务写的版本对我可见”。
当前事务在 trx_sys
中注册(分配 trx_id
);
遍历 trx_sys
全部事务列表,获取所有已开启但未提交事务,生成 m_ids[]
;
计算出 up_limit_id = min(m_ids)
,low_limit_id = 最大已分配事务ID + 1
;
将 ReadView
绑定到当前事务的上下文;
后续查询使用此 ReadView
判断版本可见性。
+---------------------+
| 当前事务 trx_id |
+---------------------+
|
第一次执行 SELECT
↓
+---------------------+
| 构建 ReadView |
|---------------------|
| m_ids: [101, 102] | ← 当前活跃事务ID
| creator_id: 103 |
| up_limit_id: 101 |
| low_limit_id: 105 |
+---------------------+
↓
遍历记录版本链(每条记录)
↓
对于记录 R,版本由 trx_id = X 创建:
判断是否可见:
┌────────────┬──────────────────────┐
│ 条件 │ 是否满足 │
├────────────┼──────────────────────┤
│ X == 103 │ 当前事务写的版本 │ ✅
│ X < 101 │ 老事务,已提交 │ ✅
│ X ∈ m_ids[] │ 活跃未提交事务写的版本│ ❌
│ X ≥ 105 │ 新事务,未开始 │ ❌
└────────────┴──────────────────────┘
InnoDB 实现版本可见性的核心代码在文件 row0sel.cc
中,核心函数为:
row_search_mvcc()
内部调用了:
row_sel_build_read_view_if_needed()
trx_is_active()
row_vers_build_for_consistent_read()
row_vers_old_has_index_entry()
可见性判断核心函数是:
row_vers_is_version_consistent()
它判断给定记录版本 trx_id
是否落在当前事务的 ReadView
可见范围内。
SELECT * FROM information_schema.innodb_trx;
输出字段包括:
trx_id
trx_started
trx_state
trx_query
SELECT * FROM performance_schema.data_locks;
隔离级别 | ReadView 特性 | MVCC 是否启用 |
---|---|---|
READ UNCOMMITTED | 不创建 ReadView | ❌ |
READ COMMITTED | 每次查询生成新的 ReadView | ✅ |
REPEATABLE READ | 第一次查询创建,整个事务复用 | ✅ |
SERIALIZABLE | 强制加锁,不依赖 MVCC | ❌(或部分) |
组件 | 描述 |
---|---|
DB_TRX_ID |
记录的创建者事务 ID |
DB_ROLL_PTR |
指向旧版本 undo 链 |
ReadView |
当前事务能看到哪些事务写的版本 |
判断逻辑 | 本事务写的、已提交、比我老 → ✅可见 |
作用场景 | 非锁定读取(如 SELECT)使用可见性判断,不加锁 |
表结构:
CREATE TABLE user (
id INT PRIMARY KEY,
name VARCHAR(20),
age INT
) ENGINE=InnoDB;
初始数据:
INSERT INTO user (id, name, age) VALUES (1, 'Alice', 20);
设置隔离级别为:REPEATABLE READ
(InnoDB 默认)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
目标:验证事务A使用的是旧快照(ReadView)
-- 会话A:
START TRANSACTION;
SELECT age FROM user WHERE id = 1; -- 读取 age = 20(创建 ReadView)
-- 会话B:
START TRANSACTION;
UPDATE user SET age = 30 WHERE id = 1;
COMMIT;
-- 会话A(再次读取):
SELECT age FROM user WHERE id = 1; -- age = 20(依旧使用 ReadView)
COMMIT;
A 的 ReadView
在 START TRANSACTION
+ SELECT
时创建;
B 的更新提交后虽然已经生效;
但 A 一直用的是旧快照,因此只能看到原值 20
;
MVCC 生效,读一致性保证。
目标:验证事务内部写入对自己始终可见
-- 会话A:
START TRANSACTION;
UPDATE user SET age = 40 WHERE id = 1; -- 修改成功
SELECT age FROM user WHERE id = 1; -- age = 40 ✅
COMMIT;
InnoDB 可见性规则中,当前事务自己创建的版本
一定可见;
不管 ReadView 怎么设置,DB_TRX_ID == 当前事务ID → 总是可见;
所以事务中写自己、读自己,肯定能看到最新的值。
目标:读未提交/读已提交隔离级别对可见性的影响
-- 会话B设置为 READ COMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
执行流程:
-- 会话A
START TRANSACTION;
UPDATE user SET age = 50 WHERE id = 1;
-- 会话B
START TRANSACTION;
SELECT age FROM user WHERE id = 1; -- 看到的是 40,A未提交
COMMIT;
-- 会话A 提交
COMMIT;
A 修改后的新版本未提交,对其他事务不可见;
B 作为 READ COMMITTED
,只能看到已提交版本;
所以仍然读到提交前的 age = 40
;
实现了避免脏读的目标。
目标:验证 ReadView 快照不能看见并发 insert
-- 会话A:
START TRANSACTION;
SELECT * FROM user WHERE age > 10; -- 看到 id=1 一条记录
-- 会话B:
START TRANSACTION;
INSERT INTO user (id, name, age) VALUES (2, 'Bob', 25);
COMMIT;
-- 会话A(再次读取):
SELECT * FROM user WHERE age > 10; -- 仍然只有 id=1 ✅
COMMIT;
会话 A 的 ReadView
不包括 B 的事务;
虽然 B 成功插入数据,但 A 的快照中根本“没这个事务”;
所以 A 看不到插入的新行,即使它“符合条件”;
这就是 MVCC 避免幻读的前提(非锁定读)。
目标:使用 SELECT ... FOR UPDATE
强制锁住间隙,阻止其他事务插入
-- 会话A:
START TRANSACTION;
SELECT * FROM user WHERE age > 10 FOR UPDATE; -- 加 next-key lock(间隙锁)
-- 会话B:
START TRANSACTION;
INSERT INTO user (id, name, age) VALUES (3, 'Carl', 22); -- 阻塞 ❗
会话 A 加了 Next-Key Lock(行锁 + 间隙锁),锁住 age > 10
范围;
B 插入的 age=22 落在该间隙中,被锁;
真正避免了幻读(防止新行插入);
这是防幻读最彻底的手段,比 MVCC 本身更强。
冲突情况 | 是否可见 | 原因 |
---|---|---|
当前事务自己创建的记录 | ✅ 可见 | DB_TRX_ID == trx_id |
已提交事务创建的版本,在视图之前提交 | ✅ 可见 | MVCC 快照可见 |
并发事务创建的新版本 | ❌ 不可见 | 在 ReadView 中不可见 |
已提交的新事务写入,但 ReadView 早于它 | ❌ 不可见 | 快照不可见 |
幻读插入记录 | ❌(MVCC) / (加锁) | MVCC 屏蔽 / Next-Key Lock 阻止 |
场景 | 建议 |
---|---|
跨多次查询要保证数据一致 | 显式 START TRANSACTION ,复用 ReadView |
并发写入读操作多 | 用 MVCC + REPEATABLE READ |
精确防幻读 | 使用 SELECT ... FOR UPDATE 锁定间隙 |
高并发插入/更新 | 监控锁冲突,避免大范围查询加锁 |
实时读取最新数据 | 使用 READ COMMITTED 隔离级别 |
我们需要从两个核心机制入手:
✅ MVCC(多版本并发控制) – 避免“读到别人新插入的数据”
✅ 间隙锁(Gap Lock) + Next-Key Lock – 阻止别人“插入我看不见的新数据”
幻读:在同一个事务中,两次相同条件的查询,返回的结果集不一样,而这个差异来自于其他事务新增的“符合条件”的行。
示例:
-- 第一次查询
SELECT * FROM orders WHERE price > 100;
-- 另一个事务插入一条新记录(price=200)
-- 第二次查询,结果多了一行
这种“新冒出来的记录”,就叫“幻影行” —— 这就是幻读。
手段 | 能否防幻 | 原理 |
---|---|---|
✅ MVCC + ReadView | 是(对普通非锁定查询) | 多版本快照,不读新事务创建的版本 |
✅ 间隙锁机制 | 是(对锁定读取) | 锁住查询范围,阻止插入/修改操作 |
在 REPEATABLE READ
下,InnoDB 对非加锁查询使用 MVCC(多版本)读取:
START TRANSACTION;
SELECT * FROM user WHERE age > 20; -- 第一次查询,生成 ReadView
-- 别的事务插入一行 age = 25
SELECT * FROM user WHERE age > 20; -- 第二次查询,不会看到新行 ✅
COMMIT;
第一次查询时创建 ReadView
后续所有查询都只读“老版本链”中对 ReadView 可见的版本
别人插入的是新版本,不在快照中,不可见
所以不会出现“幻影行”
MVCC 只是“读视图”避开幻读,并没有真正阻止幻读产生
如果你是要做更新或删除操作,MVCC 是不够的!
InnoDB 为了防止幻读,在范围查询上加的锁叫做:
Next-Key Lock
= 行锁(Record Lock)+ 间隙锁(Gap Lock)
它会把查询范围内的“已有行 + 可能插入的新位置”都锁住!
-- 原表中 age 有 10, 20, 30 三条记录
-- 查询:
SELECT * FROM user WHERE age > 15 FOR UPDATE;
-- 加锁范围:
(15,20]、(20,30]、(30, +∞)
所有可能插入符合 age > 15
条件的点,都被锁住了。
范围:age > 15
┌─────┬────────┬────────┬────────────┐
│ 10 │ 20 │ 30 │ ∞ │
└─────┴────────┴────────┴────────────┘
↑ ↑ ↑ ↑
└─ (15,20] (20,30] (30,+∞) ← 都被加锁
其他事务无法在这些“间隙”中插入新记录 ✅
-- 会话A
START TRANSACTION;
SELECT * FROM user WHERE age > 15 FOR UPDATE;
-- 会话B(阻塞 ❌)
INSERT INTO user (id, name, age) VALUES (4, 'Bob', 25); -- 被间隙锁阻止
会话 A 持有 (15, +∞)
的间隙锁,B 插入的 age = 25
正好落在间隙中,阻塞等待。
InnoDB 使用 B+ Tree 存储记录,查询过程中会:
扫描范围:定位满足 WHERE
条件的记录位置
对这些记录本身加行锁(Record Lock)
对这些记录之间的空隙,加间隙锁(Gap Lock)
┌────────────┐
│ key = 10 │
├────────────┤
│ key = 20 │
├────────────┤
│ key = 30 │
└────────────┘
对于 SELECT ... WHERE key > 15 FOR UPDATE
:
加锁 key=20、30(Record Lock)
同时加锁 (15,20)、(20,30)、(30,+∞) 的间隙(Gap Lock)
InnoDB 核心函数:
row_search_for_mysql()
└─ row_sel_build_range_access_path()
└─ sel_set_rec_lock() → 加锁逻辑
锁的结构体:
struct lock_t {
ulint type_mode; // record/gap/next-key
rec_t* record;
dict_index_t* index;
};
间隙锁和 next-key lock 会设置特定位标识,并与索引项绑定。
-- A:
START TRANSACTION;
SELECT * FROM user WHERE age > 20;
-- B:
INSERT INTO user (id, name, age) VALUES (10, 'New', 25);
COMMIT;
-- A:
SELECT * FROM user WHERE age > 20; -- 多了一条 ✅,发生幻读
-- A:
START TRANSACTION;
SELECT * FROM user WHERE age > 20 FOR UPDATE;
-- B:
INSERT INTO user (id, name, age) VALUES (10, 'New', 25); -- 阻塞 ❌
机制 | 防止方式 | 场景 |
---|---|---|
MVCC + ReadView | 不读新插入版本 | 普通非锁定查询(如 SELECT) |
Next-Key Lock | 直接阻止插入 | 范围查询+更新/删除等操作 |
间隙锁 | 锁定插入位置 | 范围内插入被阻塞 |
行锁 | 仅锁定已有记录 | 无法防止幻读 |
应用场景 | 推荐写法 |
---|---|
严格防止幻读(如金融场景) | 使用 SELECT ... FOR UPDATE |
普通查询,关注一致性 | 使用 REPEATABLE READ 即可 |
插入并发冲突 | 配合唯一约束 + 锁 |
范围更新/删除 | 明确加锁防幻插入 |
-- 创建测试表
CREATE TABLE user (
id INT PRIMARY KEY,
name VARCHAR(20),
age INT
) ENGINE=InnoDB;
-- 插入初始数据
INSERT INTO user VALUES (1, 'Alice', 20);
INSERT INTO user VALUES (2, 'Bob', 25);
设置隔离级别为默认的 REPEATABLE READ
(InnoDB 默认使用 MVCC + Next-Key Lock)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
测试非加锁读是否看到新插入的符合条件的记录?
用于演示 MVCC 防止幻读
START TRANSACTION;
SELECT * FROM user WHERE age > 22;
-- 返回:id=2, Bob, 25(仅 1 条)
-- 此时生成 ReadView 快照
START TRANSACTION;
INSERT INTO user VALUES (3, 'Carl', 30); -- 满足 age > 22
COMMIT;
SELECT * FROM user WHERE age > 22;
-- 结果仍然是:id=2, Bob, 25 ✅
-- 并未出现 Carl 的记录
幻读 没有发生
虽然会话B插入了一条符合条件的记录,但事务A使用的是快照 ReadView
新插入的版本不在当前事务快照内,不可见
FOR UPDATE
显式加锁是否能阻止插入?(防幻插入)测试 SELECT ... FOR UPDATE
是否会锁住插入间隙
演示 间隙锁 + Next-Key Lock 阻止插入
START TRANSACTION;
SELECT * FROM user WHERE age > 22 FOR UPDATE;
-- 会锁住已有记录(age=25),以及它之后的间隙(25,+∞)
START TRANSACTION;
INSERT INTO user VALUES (4, 'David', 28); -- 落入间隙范围
-- ⚠️ 阻塞!等待锁释放
COMMIT;
-- 插入现在才完成
会话A加了 Next-Key Lock(间隙锁 + 行锁)
插入操作被“锁定范围”阻塞
成功防止了幻读插入(不仅不可读,还不可写)
READ COMMITTED
+ 非加锁 SELECT 会发生幻读测试低隔离级别 READ COMMITTED
会不会看到新插入的行
验证在非加锁读下幻读是可能的
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT * FROM user WHERE age > 22;
-- 返回 id=2 (Bob, 25)
START TRANSACTION;
INSERT INTO user VALUES (5, 'Eva', 26); -- age > 22
COMMIT;
SELECT * FROM user WHERE age > 22;
-- ❗ 返回两条记录:Bob + Eva
-- 发生幻读了
因为 READ COMMITTED
隔离级别每次查询都重新构建 ReadView
所以新插入的记录对后续查询可见
幻读确实发生了
测试编号 | 场景 | 是否幻读 | 原因分析 |
---|---|---|---|
1 | REPEATABLE READ + 非锁定查询 |
❌ 没有 | MVCC + 快照一致性,插入对快照不可见 |
2 | FOR UPDATE 锁定查询范围 |
❌ 没有 | Next-Key Lock 锁住插入间隙,插入阻塞 |
3 | READ COMMITTED + 普通查询 |
✅ 有 | 每次新 ReadView,插入的新数据对后续查询可见 |
需求场景 | 推荐手段 |
---|---|
保证两次查询结果一致 | 使用 REPEATABLE READ 事务 |
严格禁止“条件插入”导致幻读 | 使用 SELECT ... FOR UPDATE |
查询实时数据 | 使用 READ COMMITTED |
高并发插入更新场景 | 控制锁粒度、加唯一索引防止锁冲突 |
INFORMATION_SCHEMA
的 3 张核心锁监控表要深入了解 InnoDB 锁的状态与机制,就必须掌握如何实时查看、分析事务加锁情况、阻塞状态、锁等待链等信息。
MySQL 提供了 INFORMATION_SCHEMA
的 3 张核心锁监控表 —— INNODB_LOCKS
, INNODB_LOCK_WAITS
, INNODB_TRX
,可以结合它们分析锁争用与事务阻塞。
从 MySQL 5.5 起,InnoDB 引入了锁监控表。
SELECT * FROM information_schema.innodb_locks;
SELECT * FROM information_schema.innodb_lock_waits;
SELECT * FROM information_schema.innodb_trx;
⚠️ 注意:
这些表只会有活动事务产生数据(即:有加锁且未提交的事务)
若你看到为空,说明当前无持锁事务
视图名称 | 作用 |
---|---|
INNODB_TRX |
展示当前所有活跃事务(事务ID、状态、运行时间、锁等待等) |
INNODB_LOCKS |
展示所有持有或等待的锁(锁类型、锁对象、模式) |
INNODB_LOCK_WAITS |
展示锁等待关系(谁等待谁) |
我们创建以下环境用于演示:
CREATE TABLE t_lock_demo (
id INT PRIMARY KEY,
val VARCHAR(20)
) ENGINE=InnoDB;
INSERT INTO t_lock_demo VALUES (1, 'a'), (2, 'b');
START TRANSACTION;
SELECT * FROM t_lock_demo WHERE id = 1 FOR UPDATE;
-- A事务锁住了 id = 1 这一行
START TRANSACTION;
UPDATE t_lock_demo SET val = 'z' WHERE id = 1;
-- ⚠️ 阻塞!因为等待 A 释放锁
SELECT * FROM information_schema.innodb_trx\G
SELECT * FROM information_schema.innodb_locks\G
SELECT * FROM information_schema.innodb_lock_waits\G
INNODB_TRX
核心字段字段名 | 含义 |
---|---|
trx_id |
当前事务 ID(MySQL 内部 ID) |
trx_state |
事务状态,如 RUNNING , LOCK WAIT 等 |
trx_started |
事务开始时间 |
trx_mysql_thread_id |
MySQL 线程 ID(可用来 KILL ) |
trx_query |
当前事务执行的 SQL 语句 |
trx_wait_started |
若等待中,等待开始时间 |
trx_rows_locked |
已加锁行数 |
Tips:
有事务为 LOCK WAIT
状态,说明它正在等待其他事务释放锁。
INNODB_LOCKS
核心字段字段名 | 含义 |
---|---|
lock_id |
锁的唯一标识 ID |
lock_mode |
锁模式(如 X , S , IX , IS 等) |
lock_type |
RECORD 、TABLE (行锁/表锁) |
lock_table |
锁在哪张表 |
lock_index |
锁在哪个索引上(通常为主键或二级索引) |
lock_data |
被锁住的主键值(记录锁)或索引范围(间隙锁) |
Tips:
多个事务锁定相同 lock_data
,说明竞争发生在这行。
INNODB_LOCK_WAITS
核心字段字段名 | 含义 |
---|---|
requesting_trx_id |
正在等待锁的事务 ID |
blocking_trx_id |
持有锁的事务 ID |
requested_lock_id |
等待中的锁 ID |
blocking_lock_id |
阻塞它的锁 ID |
Tips:
这是分析死锁/阻塞链的关键视图!
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query
FROM information_schema.innodb_trx;
SELECT r.trx_id AS waiting_trx,
r.trx_mysql_thread_id AS waiting_thread,
r.trx_query AS waiting_query,
b.trx_id AS blocking_trx,
b.trx_mysql_thread_id AS blocking_thread,
b.trx_query AS blocking_query
FROM information_schema.innodb_lock_waits w
JOIN information_schema.innodb_trx r ON w.requesting_trx_id = r.trx_id
JOIN information_schema.innodb_trx b ON w.blocking_trx_id = b.trx_id;
-- innodb_trx:
trx_id: 1234
trx_state: LOCK WAIT
trx_query: UPDATE t_lock_demo SET val='z' WHERE id = 1;
trx_id: 1233
trx_state: RUNNING
trx_query: SELECT * FROM t_lock_demo WHERE id=1 FOR UPDATE;
-- innodb_lock_waits:
requesting_trx_id: 1234
blocking_trx_id: 1233
-- innodb_locks:
lock_id: 1233:45:3
lock_mode: X
lock_type: RECORD
lock_table: test/t_lock_demo
lock_data: 1
分析:
事务1233 已锁住了 id = 1
,持有 X
锁
事务1234 想更新同一行,被阻塞
阻塞链清晰、可追踪
目标 | 工具/建议 |
---|---|
观察事务是否阻塞 | 查看 innodb_trx |
分析锁等待链 | 联表 innodb_trx + lock_waits |
分析哪个键被锁住 | innodb_locks.lock_data |
手动终止阻塞事务 | KILL |
定期排查锁争用 | 使用 performance_schema.events_waits_current |
工具 | 用途 |
---|---|
INNODB_TRX |
显示当前事务状态 |
INNODB_LOCKS |
显示每个事务持有/等待的锁 |
INNODB_LOCK_WAITS |
显示锁等待依赖关系(谁等谁) |
联表分析(推荐) | 快速定位死锁/阻塞事务链 |
performance_schema |
MySQL 8.0 更强锁监控 |