MySQL知识梳理(3)------事务与锁机制
作者:没有四次元口袋的蓝胖
日期:2026-06-21
标签:MySQL, 事务, 锁
思维导图速览
MySQL事务与锁机制
├── 1. ACID特性
│ ├── A-原子性:Undo Log实现
│ ├── C-一致性:数据库约束保证
│ ├── I-隔离性:锁机制实现
│ └── D-持久性:Redo Log实现
├── 2. 事务隔离级别
│ ├── READ UNCOMMITTED(读未提交)
│ ├── READ COMMITTED(读已提交)
│ ├── REPEATABLE READ(可重复读)⭐MySQL默认
│ └── SERIALIZABLE(串行化)
├── 3. 三种并发问题
│ ├── 脏读:读取未提交数据
│ ├── 不可重复读:同一事务内数据不一致
│ └── 幻读:同一事务内结果集不一致
├── 4. 隔离级别实现方式
│ ├── 锁实现
│ └── MVCC快照机制
├── 5. 锁分类
│ ├── 按粒度:表锁/行锁
│ └── 按类型:S锁/X锁
├── 6. InnoDB锁算法
│ ├── 记录锁(Record Lock)
│ ├── 间隙锁(Gap Lock)
│ └── Next-Key Lock ⭐InnoDB默认
├── 7. 死锁
│ ├── 概念与产生条件
│ ├── MySQL自动检测机制
│ └── 避免死锁5条策略
├── 8. 分布式事务
│ ├── 2PC二阶段提交
│ ├── 3PC三阶段提交
│ ├── TCC补偿式事务
│ └── 本地消息表
└── 9. 事务使用注意事项
└── 5条最佳实践
一、ACID特性
⚡ 面试高频:什么是事务的ACID特性?
ACID是事务的四大特性,确保数据库操作的可靠性和一致性:
| 特性 | 全称 | 说明 | MySQL实现原理 |
|---|---|---|---|
| A - Atomicity | 原子性 | 事务是最小执行单位,要么全部成功,要么全部失败回滚 | Undo Log(回滚日志) |
| C - Consistency | 一致性 | 事务执行前后,数据库状态保持一致(合法状态) | 数据库本身约束(外键/唯一索引等) |
| I - Isolation | 隔离性 | 并发事务相互隔离,互不干扰 | 锁机制 + MVCC |
| D - Durability | 持久性 | 事务提交后,数据永久保存 | Redo Log(重做日志) |
核心要点
1. 原子性:由Undo Log实现
sql
-- Undo Log记录数据修改前的旧值
-- 事务回滚时,通过逆向SQL恢复旧值
UPDATE users SET age = 20 WHERE id = 1;
-- Undo Log记录:id=1的age从20变为18(旧值)
2. 持久性:由Redo Log实现
事务提交 → 数据写入Redo Log(持久化) → 后台刷盘
崩溃恢复时,读取Redo Log重做未刷盘的数据
3. 隔离性:由锁机制实现
读已提交(READ COMMITTED) ← 每次读取生成新快照
可重复读(REPEATABLE READ) ← 事务开始时生成快照
⚠️ 面试坑点 :很多人误以为一致性是MySQL保证的,实际上一致性是由业务逻辑 + 数据库约束共同保证的,MySQL只是提供了工具。
二、事务隔离级别
⚡ 面试高频:MySQL有哪些隔离级别?分别解决什么问题?
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| READ UNCOMMITTED(读未提交) | ✅ 可能发生 | ✅ 可能发生 | ✅ 可能发生 | 直接读取最新值,性能最高,安全性最差 |
| READ COMMITTED(读已提交) | ❌ 不可能 | ✅ 可能发生 | ✅ 可能发生 | 每次读取生成新快照 |
| REPEATABLE READ(可重复读)⭐ | ❌ 不可能 | ❌ 不可能 | ✅ 可能发生 | 事务开始时生成快照(MySQL默认) |
| SERIALIZABLE(串行化) | ❌ 不可能 | ❌ 不可能 | ❌ 不可能 | 所有读操作加锁,串行执行,性能最差 |
核心要点
1. MySQL默认隔离级别:REPEATABLE READ
sql
-- 查看当前会话隔离级别
SELECT @@tx_isolation;
-- 设置隔离级别(会话级)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
2. 三种并发问题详解
脏读:事务A读取了事务B未提交的数据,事务B回滚后,数据成了"脏数据"
场景:张三转100给李四,事务A读取到转账成功,但事务B实际回滚了
不可重复读:同一事务内,两次读取同一行数据结果不同(其他事务UPDATE了数据)
场景:事务A两次读取账户余额,中间事务B修改了余额
幻读:同一事务内,两次查询结果集行数不同(其他事务INSERT/DELETE了行)
场景:事务A两次查询用户列表,中间事务B新增了一条用户记录
⚠️ 面试坑点 :InnoDB在REPEATABLE READ级别下通过Next-Key Lock解决了幻读问题,很多面试者只知道MVCC,这是常见的遗漏。
三、隔离级别实现方式
⚡ 面试高频:各隔离级别是怎么实现的?
核心要点
1. 锁实现方式
sql
-- READ UNCOMMITTED:无锁,直接读取最新版本
-- 性能最高,但可能读到脏数据
-- READ COMMITTED:每次读取生成新ReadView
-- 保证读取的是已提交的数据
-- REPEATABLE READ:事务开始时生成ReadView
-- 整个事务内多次读取结果一致
-- SERIALIZABLE:所有SELECT隐式加锁
SELECT * FROM users; -- 自动变为 SELECT * FROM users LOCK IN SHARE MODE
2. MVCC(多版本并发控制)核心概念
┌─────────────────────────────────────────────────────────┐
│ MVCC 工作原理 │
├─────────────────────────────────────────────────────────┤
│ 每行数据有两个隐藏列: │
│ • db_trx_id:最后一次修改的事务ID │
│ • db_roll_ptr:指向Undo Log的指针 │
│ │
│ ReadView(快照)包含: │
│ • m_ids:活跃事务ID列表 │
│ • min_trx_id:最小活跃事务ID │
│ • max_trx_id:创建快照时最大事务ID+1 │
│ • creator_trx_id:当前事务ID │
└─────────────────────────────────────────────────────────┘
3. MVCC读取规则
sql
-- 读取时判断数据版本:
-- 1. 数据的trx_id < min_trx_id → 已提交,可读
-- 2. 数据的trx_id 在m_ids中 → 未提交,不可读,查找Undo Log
-- 3. 数据的trx_id >= max_trx_id → 快照后开启的不可读
⚠️ 面试坑点 :MVCC只解决了普通SELECT 的幻读问题,当前读(SELECT...FOR UPDATE、INSERT、UPDATE、DELETE)仍需锁来保证。
四、锁分类
⚡ 面试高频:InnoDB有哪些锁类型?
按锁粒度分类
| 类型 | 锁住对象 | 开销 | 并发度 | 适用场景 |
|---|---|---|---|---|
| 表锁 | 整张表 | 小 | 低 | MyISAM引擎、全表扫描 |
| 行锁 | 单行/多行 | 大 | 高 | InnoDB引擎、精准操作 |
sql
-- 表锁(MyISAM默认,加锁释锁快)
LOCK TABLES users READ; -- 表级读锁
LOCK TABLES users WRITE; -- 表级写锁
UNLOCK TABLES; -- 释放锁
-- 行锁(InnoDB默认,锁住满足条件的行)
SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 锁定id=1这行
按锁类型分类
| 类型 | 名称 | 作用 | 语法 |
|---|---|---|---|
| S锁 | 共享锁(Shared Lock) | 允许其他事务读取,不可修改 | SELECT ... LOCK IN SHARE MODE |
| X锁 | 排他锁(Exclusive Lock) | 禁止其他事务读写 | SELECT ... FOR UPDATE / INSERT / UPDATE / DELETE |
锁兼容性矩阵
| S锁请求 | X锁请求 | |
|---|---|---|
| 已有S锁 | ✅ 兼容 | ❌ 不兼容 |
| 已有X锁 | ❌ 不兼容 | ❌ 不兼容 |
sql
-- 事务A
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE; -- 加S锁
-- 事务B(可执行)
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE; -- ✅ 可加S锁
-- 事务B(不可执行)
SELECT * FROM users WHERE id = 1 FOR UPDATE; -- ❌ X锁与S锁冲突,等待
UPDATE users SET age = 20 WHERE id = 1; -- ❌ X锁与S锁冲突,等待
⚠️ 面试坑点 :很多人以为行锁就是锁住整行,实际上InnoDB行锁锁定的是索引记录 ,如果字段没有索引,会升级为表锁。
五、InnoDB锁算法
⚡ 面试高频:什么是记录锁、间隙锁、Next-Key Lock?
三种锁算法对比
| 算法 | 锁定范围 | 作用 |
|---|---|---|
| 记录锁(Record Lock) | 单个索引记录 | 锁定特定行 |
| 间隙锁(Gap Lock) | 索引记录之间的间隙 | 锁定范围,防止插入 |
| Next-Key Lock | 记录锁 + 间隙锁的组合 | InnoDB默认算法 |
核心要点
1. 记录锁(Record Lock)
sql
-- 锁定id=1这一行记录
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 锁定范围:[id=1]
2. 间隙锁(Gap Lock)
sql
-- 锁定id在(1,10)这个区间
SELECT * FROM users WHERE id BETWEEN 3 AND 7 FOR UPDATE;
-- 锁定范围:(1, 3), (3, 7), (7, 10) 之间的间隙
-- 注意:Gap Lock锁定的是间隙本身,不是具体的行
3. Next-Key Lock(组合锁)
sql
-- 假设表中有id=1, 3, 5, 7, 9的数据
SELECT * FROM users WHERE id = 5 FOR UPDATE;
-- 锁定范围:[3, 5) + id=5 = [3, 5] + (5, 7) = [3, 7)
-- 即:锁定id=5这行,同时锁定(5,7)这个间隙
-- 实际效果:
-- • 防止其他事务在id=5周围插入新记录(解决幻读)
-- • 锁定当前读取的行(防止修改/删除)
4. InnoDB如何解决幻读
在REPEATABLE READ级别下:
• 普通SELECT(快照读):通过MVCC解决幻读
• SELECT...FOR UPDATE/UPDATE/DELETE(当前读):通过Next-Key Lock解决幻读
Next-Key Lock锁定:当前行 + 前后的间隙
效果:其他事务无法在锁定范围内INSERT新记录
⚠️ 面试坑点:唯一索引配合等值查询时,Next-Key Lock会退化为记录锁(因为唯一性保证了不会有"间隙")。
六、死锁
⚡ 面试高频:什么是死锁?如何避免死锁?
死锁定义
死锁:两个或多个事务相互等待对方持有的锁,形成循环等待,无法继续执行。
事务A:持有锁1,等待锁2
事务B:持有锁2,等待锁1
形成循环等待 → 死锁
死锁示例
sql
-- 事务A
BEGIN;
UPDATE users SET age = 20 WHERE id = 1; -- 锁定id=1
UPDATE users SET age = 21 WHERE id = 2; -- 等待锁定id=2
-- 事务B(同时执行)
BEGIN;
UPDATE users SET age = 30 WHERE id = 2; -- 锁定id=2
UPDATE users SET age = 31 WHERE id = 1; -- 等待锁定id=1
-- ⚠️ 死锁!相互等待对方的锁
MySQL死锁处理机制
sql
-- 1. InnoDB自动检测死锁
-- 默认开启死锁检测(innodb_deadlock_detect = ON)
-- 检测到死锁后,回滚"事务量最小"(undo最少)的事务
-- 2. 超时回滚机制
-- innodb_lock_wait_timeout = 50(默认50秒)
-- 等待锁超时后自动回滚
避免死锁的5条策略
⚡ 避免死锁核心原则:按固定顺序访问资源,缩短锁持有时间
| 策略 | 说明 | 示例 |
|---|---|---|
| 1. 统一顺序访问 | 多表/多行操作时,按固定顺序访问 | 先操作id小的,再操作id大的 |
| 2. 降低隔离级别 | 读已提交(READ COMMITTED)可减少锁范围 | 业务允许时使用 |
| 3. 减少锁持有时间 | 减少事务内操作,快提交快释放 | 避免长事务 |
| 4. 合理使用锁 | 非必要不加锁,善用MVCC | 读多写少用快照读 |
| 5. 添加适当索引 | 减少锁的粒度,避免升级为表锁 | 为WHERE条件列建索引 |
sql
-- 错误示范:按不同顺序更新,导致死锁
-- 事务A:先更新id=1,再更新id=2
-- 事务B:先更新id=2,再更新id=1
-- → 容易死锁
-- 正确示范:统一按id从小到大顺序
-- 事务A:先更新id=1,再更新id=2
-- 事务B:先更新id=1,再更新id=2
-- → 不会死锁
⚠️ 面试坑点:死锁和锁等待不是一回事。锁等待是正常的(事务A等事务B的锁),死锁是循环等待(相互等待)。
七、分布式事务
⚡ 面试高频:什么是分布式事务?有哪些解决方案?
为什么需要分布式事务
单体应用:所有操作在一个数据库 → 本地事务即可
微服务/分库分表:操作涉及多个数据库 → 需要分布式事务
解决方案对比
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 2PC(二阶段提交) | Prepare → Commit | 强一致性 | 同步阻塞、单点故障、数据不一致风险 | 跨库操作 |
| 3PC(三阶段提交) | CanCommit → PreCommit → DoCommit | 引入超时机制 | 仍可能数据不一致 | 追求可用性 |
| TCC(补偿式) | Try-Confirm-Cancel | 性能好、无锁 | 业务侵入性强 | 高并发场景 |
| 本地消息表 | 本地事务 + 异步消息 | 实现简单 | 有最终一致性延迟 | 异步解耦 |
核心要点
1. 2PC(二阶段提交)
阶段1(Prepare):
协调者向所有参与者发送Prepare请求
参与者锁定资源,返回"准备就绪"或"失败"
阶段2(Commit/Rollback):
所有参与者都准备就绪 → 发送Commit,提交事务
任一参与者失败 → 发送Rollback,回滚所有事务
2. TCC(Try-Confirm-Cancel)
sql
-- Try:预留资源
UPDATE account SET frozen = frozen + 100 WHERE user_id = 1;
-- Confirm:确认执行(Try成功后调用)
UPDATE account SET balance = balance + 100, frozen = frozen - 100;
-- Cancel:取消回滚(Try失败时调用)
UPDATE account SET frozen = frozen - 100;
3. 本地消息表
┌──────────────────────────────────────────────────────────┐
│ 本地事务 + 消息队列实现最终一致性 │
│ │
│ 1. 本地事务操作 + 消息记录在同一数据库 │
│ 2. 消息消费者处理失败 → 重试 → 人工补偿 │
│ 3. 定时任务扫描未处理消息 → 重新投递 │
└──────────────────────────────────────────────────────────┘
MySQL XA事务语法
sql
-- 开启XA事务
XA START 'transaction_001';
-- 执行SQL
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
UPDATE orders SET status = 'paid' WHERE order_id = 10086;
-- 结束事务
XA END 'transaction_001';
-- 准备阶段(所有参与者锁定资源)
XA PREPARE 'transaction_001';
-- 提交阶段(所有参与者提交事务)
XA COMMIT 'transaction_001';
-- 如果需要回滚
XA ROLLBACK 'transaction_001';
⚠️ 面试坑点:分布式事务无法做到像本地事务一样的强一致性,只能实现最终一致性。根据业务场景选择合适方案,不要盲目追求强一致性。
八、事务使用注意事项
⚡ 面试高频:事务使用有哪些坑?
5条最佳实践
| 原则 | 说明 | 错误示例 | 正确做法 |
|---|---|---|---|
| 1. 事务范围要小 | 事务越小,锁持有时间越短 | BEGIN后执行几十条SQL | 最小化事务范围 |
| 2. 避免长事务 | 长事务占用大量锁和日志,影响并发 | 事务内包含查询报表操作 | 拆分大事务为小事务 |
| 3. 避免复杂查询 | 复杂查询可能导致大量锁 | 事务内执行全表扫描 | 使用索引,减少锁范围 |
| 4. 异常必须回滚 | 未捕获异常会导致数据不一致 | 只写INSERT不处理异常 | 使用try-catch + rollback |
| 5. DDL自动提交 | CREATE/ALTER/DROP会触发隐式提交 | 在事务中执行ALTER TABLE | DDL不参与事务 |
核心要点
1. 事务的正确写法
sql
BEGIN;
try {
UPDATE users SET balance = balance - 100 WHERE id = 1;
UPDATE users SET balance = balance + 100 WHERE id = 2;
INSERT INTO transfer_log (...) VALUES (...);
COMMIT;
} catch (Exception $e) {
ROLLBACK; -- 必须回滚
throw $e;
}
2. DDL语句会隐式提交
sql
BEGIN;
INSERT INTO users (name) VALUES ('张三');
ALTER TABLE users ADD COLUMN age INT; -- ⚠️ 隐式提交!INSERT被自动提交
ROLLBACK; -- 无法回滚INSERT
3. 监控长事务
sql
-- 查看运行时间超过60秒的事务
SELECT * FROM information_schema.INNODB_TRX
WHERE trx_started < NOW() - INTERVAL 60 SECOND;
-- 查看锁等待
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
⚠️ 面试坑点 :
SET autocommit = 0后忘记手动COMMIT会导致事务一直开着,连接被占满。生产环境建议使用START TRANSACTION。
写在最后
学习建议
⚡ 面试Tips:事务与锁是MySQL高频考点,必须深入理解原理
1. 理解原理,而非死记硬背
ACID、隔离级别、锁机制不仅要记住,更要理解"为什么":
- 为什么需要原子性?(数据一致性保障)
- 为什么默认是REPEATABLE READ?(平衡并发与安全)
- 为什么用MVCC?(读写不阻塞,提高并发)
2. 动手实践验证
• 开启两个MySQL客户端,模拟脏读、不可重复读、幻读
• 使用SHOW ENGINE INNODB STATUS查看锁信息
• 执行死锁场景,观察MySQL如何处理
3. 关联学习,形成体系
事务 ←→ 锁 ←→ MVCC ←→ 日志(Redo Log/Undo Log)
↓ ↓
隔离级别 隔离级别实现
理解各知识点之间的关联,比单独记忆更有效。
4. 掌握常见面试题解答思路
- "MySQL如何解决幻读?" → MVCC + Next-Key Lock
- "事务的隔离级别?" → 4个级别 + 脏读/不可重复读/幻读对照
- "如何避免死锁?" → 统一顺序 + 减少锁时间 + 合理索引
5. 关注实际应用
- 长事务监控与优化
- 分布式事务选型
- 业务场景下的隔离级别选择