文章

MySQL 事务与锁机制

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 COMMITTEDREPEATABLE 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-14ACID + 隔离级别 + MVCC + 锁
深入理解待定ReadView 源码、Purge 线程机制

状态: 📖 已掌握 下次复习日期: 2026-08-14