MySQL事务ACID与隔离级别
Spring 事务用起来简单,但失效场景非常多。很多人遇到过 @Transactional 不生效的情况,却不知道原因。本文把日常开发中常见的坑和对应的排查思路整理出来,帮你避免踩坑。
一、事务基础
#
1.1 什么是事务
事务是一组逻辑操作的集合,要么全部成功,要么全部失败。
START TRANSACTION; |
#
1.2 事务控制语句
-- 开启事务 |
#
1.3 自动提交
-- 查看自动提交状态 |
二、ACID特性
#
2.1 原子性(Atomicity)
定义:事务中的所有操作要么全部完成,要么全部不完成。
实现原理:Undo Log(回滚日志)
事务执行过程: |
#
2.2 一致性(Consistency)
定义:事务执行前后,数据库从一个一致状态转换到另一个一致状态。
含义:
- 数据库完整性约束不被破坏
- 外键约束、触发器等正常执行
- 业务规则保证(如转账总额不变)
实现:原子性、隔离性、持久性共同保证一致性。
#
2.3 隔离性(Isolation)
定义:多个事务并发执行时,一个事务的执行不应影响其他事务。
实现原理:
- 锁机制(Locking)
- 多版本并发控制(MVCC)
#
2.4 持久性(Durability)
定义:事务一旦提交,对数据库的改变就是永久的。
实现原理:
- Redo Log(重做日志):保证已提交事务不丢失
- Force Log at Commit:事务提交时强制刷盘
事务提交时的写盘流程: |
三、并发问题
#
3.1 脏读(Dirty Read)
现象:读取到其他事务未提交的数据。
时间线: |
解决:禁止读取未提交数据(Read Committed及以上)。
#
3.2 不可重复读(Non-repeatable Read)
现象:同一事务内两次读取同一数据,结果不同。
时间线: |
解决:Repeatable Read及以上隔离级别。
#
3.3 幻读(Phantom Read)
现象:同一事务内两次查询,第二次查到了第一次没有的行。
时间线: |
解决:Serializable隔离级别,或InnoDB的间隙锁(Gap Lock)。
#
3.4 丢失更新(Lost Update)
现象:两个事务同时修改同一数据,后提交的事务覆盖了先提交的修改。
时间线: |
解决:使用乐观锁(版本号)或悲观锁(SELECT FOR UPDATE)。
四、隔离级别
#
4.1 四种隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 不可能 | 可能 | 可能 |
| REPEATABLE READ | 不可能 | 不可能 | 可能(InnoDB解决) |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 |
#
4.2 READ UNCOMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; |
- 可以读取未提交的数据
- 性能最好,但数据一致性最差
- 生产环境不建议使用
#
4.3 READ COMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; |
- 只能读取已提交的数据(解决脏读)
- 每次SELECT生成新的ReadView
- Oracle、SQL Server默认级别
- 可能出现不可重复读和幻读
#
4.4 REPEATABLE READ(MySQL默认)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; |
- 同一事务内多次读取结果一致(解决不可重复读)
- 事务开始时生成ReadView,整个事务复用
- InnoDB通过MVCC + 间隙锁,一定程度上解决幻读
#
4.5 SERIALIZABLE
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; |
- 所有操作串行执行
- 通过加锁实现,性能最差
- 完全解决所有并发问题
#
4.6 查看和设置隔离级别
-- 查看当前隔离级别 |
五、InnoDB的MVCC实现
#
5.1 隐藏字段
InnoDB每行记录都有两个隐藏字段:
- DB_TRX_ID:最后修改该记录的事务ID
- DB_ROLL_PTR:回滚指针,指向undo log
+----------+--------+---------+------------+-------------+ |
#
5.2 Undo Log链
每次修改记录,都会生成undo log,形成版本链:
当前记录(最新版本) |
#
5.3 ReadView机制
ReadView是事务进行快照读时生成的读视图,决定能看到哪个版本的数据。
ReadView包含:
- creator_trx_id:创建该ReadView的事务ID
- m_ids:生成ReadView时,活跃的事务ID列表
- min_trx_id:m_ids中的最小值
- max_trx_id:下一个要分配的事务ID
可见性判断规则:
对于某条记录的DB_TRX_ID:
- 如果DB_TRX_ID == creator_trx_id:自己修改的,可见
- 如果DB_TRX_ID < min_trx_id:在ReadView创建前已提交,可见
- 如果DB_TRX_ID >= max_trx_id:在ReadView创建后启动,不可见
- 如果min_trx_id <= DB_TRX_ID < max_trx_id:
- 在m_ids列表中:事务活跃,不可见
- 不在m_ids列表中:已提交,可见
不可见时:通过DB_ROLL_PTR找到上一个版本,继续判断。
#
5.4 不同隔离级别的ReadView差异
READ COMMITTED:
- 每次SELECT生成新的ReadView
- 能看到其他事务已提交的最新数据
REPEATABLE READ:
- 事务第一次SELECT时生成ReadView
- 整个事务期间复用该ReadView
- 保证同一事务内读取一致性
六、锁机制与隔离级别
#
6.1 InnoDB锁类型
按粒度:
- 行锁(Record Lock)
- 间隙锁(Gap Lock)
- 临键锁(Next-Key Lock = Record Lock + Gap Lock)
- 表锁
按功能:
- 共享锁(S Lock):读锁
- 排他锁(X Lock):写锁
#
6.2 各隔离级别的锁行为
READ UNCOMMITTED:
- SELECT不加锁,直接读最新数据(包括未提交的)
READ COMMITTED:
- SELECT不加锁,使用MVCC读取已提交数据
- UPDATE/DELETE只锁匹配的行(记录锁)
REPEATABLE READ:
- SELECT不加锁,使用MVCC读取快照
- UPDATE/DELETE使用临键锁(Next-Key Lock),防止幻读
- 范围查询时加间隙锁
SERIALIZABLE:
- 所有SELECT加共享锁
- 完全串行化
#
6.3 幻读的解决
InnoDB在REPEATABLE READ下通过间隙锁解决幻读:
-- T1 |
注意:普通SELECT(快照读)不受间隙锁保护,可能看到幻读。只有FOR UPDATE(当前读)加间隙锁才能完全避免。
七、事务优化建议
#
7.1 事务设计原则
- 保持事务简短:长事务占用锁时间长,影响并发
- 避免在事务中做RPC调用:网络延迟导致事务长时间不提交
- 按固定顺序访问资源:减少死锁概率
- 合适的隔离级别:不需要RR时,使用RC提升并发
#
7.2 大事务处理
-- 大DELETE分批处理 |
#
7.3 监控长事务
-- 查看正在执行的事务 |
八、总结
| 特性 | 实现机制 | 关键概念 |
|---|---|---|
| 原子性 | Undo Log | 回滚 |
| 一致性 | ACID综合 | 约束、触发器 |
| 隔离性 | MVCC + 锁 | ReadView、间隙锁 |
| 持久性 | Redo Log | WAL、刷盘 |
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现 |
|---|---|---|---|---|
| RU | ✓ | ✓ | ✓ | 无 |
| RC | ✗ | ✓ | ✓ | MVCC |
| RR | ✗ | ✗ | 部分 | MVCC + 间隙锁 |
| Serializable | ✗ | ✗ | ✗ | 串行化 |
理解ACID和隔离级别的实现原理,有助于:
- 合理选择隔离级别
- 设计出高效的事务逻辑
- 快速定位和解决并发问题
核心要点
事务失效的常见原因:非 public 方法、自调用、异常被吞掉、错误的传播级别
事务传播级别决定了方法之间的事务关系,REQUIRED 是默认值
使用 @Transactional(rollbackFor = Exception.class) 确保异常回滚
编程式事务在某些场景下比声明式事务更灵活
总结
事务管理是保证数据一致性的关键。理解事务的工作机制和常见陷阱,能帮你写出更健壮的代码。在实际项目中,合理配置事务边界非常重要。