MySQL知识梳理(3)

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. 关注实际应用

  • 长事务监控与优化
  • 分布式事务选型
  • 业务场景下的隔离级别选择