MySQL 事务与锁机制
概述
事务(ACID)和锁是 MySQL 保证数据一致性的两大支柱。理解 MVCC 多版本并发控制是分析死锁、长事务、读写性能问题的前提。
事务四大特性 (ACID)
| 特性 | 含义 | InnoDB 实现 | 与 K8s/分布式相关性 |
|---|
| 原子性 Atomicity | 要么全做,要么全不做 | Undo Log 回滚 | 分布式事务 2PC/Saga |
| 一致性 Consistency | 事务前后数据满足约束 | Redo Log + Undo Log + 锁 | 最终一致性(CAP) |
| 隔离性 Isolation | 并发事务互不干扰 | MVCC + 锁 | 隔离级别选择影响性能 |
| 持久性 Durability | 提交后不丢失 | Redo Log + WAL + Doublewrite | 分布式共识(Raft/Paxos) |
四种隔离级别
-- 查看当前隔离级别
SHOW VARIABLES LIKE 'transaction_isolation';
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | InnoDB 默认 |
|---|
| READ UNCOMMITTED | ✅ | ✅ | ✅ | ❌ |
| READ COMMITTED | ❌ | ✅ | ✅ | ❌ |
| REPEATABLE READ | ❌ | ❌ | 部分解决 | ✅ MySQL 默认 |
| SERIALIZABLE | ❌ | ❌ | ❌ | ❌ |
实际使用建议
| 场景 | 推荐级别 | 原因 |
|---|
| 金融/对账系统 | SERIALIZABLE | 强一致性需求 |
| 一般 OLTP 业务 | READ COMMITTED | 性能好,间隙锁少,符合 PostgreSQL/Oracle 默认行为 |
| 需要一致性读的快照 | REPEATABLE READ | 同一事务多次读取结果一致(如报表) |
互联网公司大多用 READ COMMITTED。 配合行锁 + 乐观锁/版本号解决绝大多数并发问题,避免 RR 下的间隙锁带来的锁冲突。
MVCC 多版本并发控制
核心机制
MVCC = Undo Log 版本链 + ReadView(一致性视图)
每条记录有两个隐藏列:
- DB_TRX_ID(最近修改的事务 ID)
- DB_ROLL_PTR(指向 Undo Log 的回滚指针)
读的时候:
- 不直接读最新数据
- 通过 ReadView 判断哪个版本可见
- 找到符合条件的 Undo Log 版本
RC vs RR 的 ReadView 差异
| 行为 | READ COMMITTED | REPEATABLE READ |
|---|
| ReadView 生成时机 | 每次 SELECT 都生成 | 事务中第一次 SELECT 时生成 |
| 效果 | 每次读到最新提交 | 整个事务内读取一致 |
RR 快照读示例:
事务 A: SELECT balance FROM accounts WHERE id=1; -- 读到 100
事务 B: UPDATE accounts SET balance=200 WHERE id=1; COMMIT;
事务 A: SELECT balance FROM accounts WHERE id=1; -- 仍然读到 100 ✅
RC 下第二次 SELECT 读到 200
当前读 vs 快照读
-- 快照读(Snapshot Read):普通 SELECT,基于 MVCC
SELECT * FROM accounts WHERE id = 1;
-- 当前读(Current Read):读取最新版本,并加锁
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- 排他锁
SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE; -- 共享锁
UPDATE / DELETE / INSERT -- 隐式当前读
InnoDB 锁机制
锁粒度
| 锁类型 | 粒度 | 并发度 | 死锁风险 |
|---|
| 表锁 | 整个表 | 低 | 低 |
| 行锁 (Record Lock) | 单行记录 | 高 | 中 |
| 间隙锁 (Gap Lock) | 索引记录间的间隙 | 中 | 高 |
| 临键锁 (Next-Key Lock) | Record + Gap | 中 | 高 |
Next-Key Lock(RR 隔离级别特有)
索引值:[1, 5, 10, 15, 20]
WHERE id = 10 FOR UPDATE → Next-Key Lock 锁定 (5, 10] + (10, 15]
等于:锁住 行 10 + 间隙 (5, 10) + 间隙 (10, 15)
作用:防止幻读(其他事务不能在 (5, 15) 中插入新行)
RC 隔离级别下间隙锁是关闭的,因此死锁少很多。
意向锁(Intention Lock)
-- 行级锁之前先在表级别加意向锁(快速判断表级锁冲突)
-- IS(意向共享锁):打算加行级共享锁
-- IX(意向排他锁):打算加行级排他锁
SELECT ... LOCK IN SHARE MODE; -- IS + 行 S 锁
SELECT ... FOR UPDATE; -- IX + 行 X 锁
死锁排查
-- 开启死锁检测(默认已开启)
SET GLOBAL innodb_deadlock_detect = ON;
-- 查看最近死锁日志(关键!)
SHOW ENGINE INNODB STATUS\G
-- 搜索 "LATEST DETECTED DEADLOCK" 部分
-- 查看当前锁等待
SELECT * FROM performance_schema.data_locks;
SELECT * FROM performance_schema.data_lock_waits;
-- 查看当前事务
SELECT * FROM information_schema.INNODB_TRX;
经典死锁场景:
事务 A: UPDATE users SET balance = balance - 100 WHERE id = 1; -- 锁 id=1
UPDATE users SET balance = balance + 100 WHERE id = 2; -- 等 id=2
事务 B: UPDATE users SET balance = balance - 50 WHERE id = 2; -- 锁 id=2
UPDATE users SET balance = balance + 50 WHERE id = 1; -- 等 id=1
→ 死锁!相互等待 → InnoDB 自动回滚其中一个
避免死锁的策略:
- 固定加锁顺序:所有事务按相同的资源顺序加锁
- 缩小事务范围:尽快提交,减少持锁时间
- 使用 RC 隔离级别:减少间隙锁
- 添加合理索引:减少锁升级和锁范围
实战 SQL
查看长时间未提交的事务
SELECT
trx_id,
trx_state,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS duration_sec,
trx_mysql_thread_id,
trx_query
FROM information_schema.INNODB_TRX
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 60
ORDER BY trx_started;
查看锁等待超时配置
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout'; -- 默认 50s
-- 建议:OLTP 系统设为 5-10s,快速失败比长时间等待好
SET GLOBAL innodb_lock_wait_timeout = 10;
Undo Log 过大排查
-- 查看 Undo 使用情况
SELECT
tablespace_name,
file_name,
ROUND(allocated_size / 1024 / 1024, 2) AS allocated_mb,
ROUND(data_size / 1024 / 1024, 2) AS data_mb
FROM information_schema.INNODB_TABLESPACES
WHERE tablespace_name LIKE 'undo%';
常见问题 / 坑点
| 问题 | 原因 | 解决方案 |
|---|
| 长事务导致 Undo Log 膨胀 | 事务不提交,Undo Log 无法回收 | innodb_undo_log_truncate=ON + 定时杀长事务 |
UPDATE ... WHERE non_indexed_col 导致表锁 | 无索引时无法定位具体行,锁全表 | WHERE 条件必须走索引 |
RR 下 LOCK IN SHARE MODE 仍有幻读 | 间隙锁阻止插入,但如果有范围查询 SELECT → UPDATE 可能不一致 | 直接用 FOR UPDATE |
INSERT ... ON DUPLICATE KEY UPDATE 自增 ID 跳号 | 即使冲突也消耗了自增值 | MySQL 8.0 特性,一般可接受 |
| 大事务导致主从延迟 | Binlog 在事务提交时才写入 | 拆分成小事务批量提交 |
关联知识
参考资源
学习时间
| 阶段 | 时间 | 备注 |
|---|
| 初次学习 | 2026-07-14 | ACID + 隔离级别 + MVCC + 锁 |
| 深入理解 | 待定 | ReadView 源码、Purge 线程机制 |
状态: 📖 已掌握
下次复习日期: 2026-08-14