MySQL事务
事务是逻辑上的一组操作,要么都执行,要么都不执行。关系型数据库事务都有 ACID 特性:
适用场景:转账汇款、订单创建与库存扣减、多表联动更新等需要数据一致性的业务操作。当业务要求"全部成功或全部失败"时,必须使用事务。
- 原子性(Atomicity):事务是最小的执行单位,不允许分割。确保动作要么全部完成,要么完全不起作用
- 一致性(Consistency):执行事务前后,数据保持一致。例如转账业务中,无论事务是否成功,转账者和收款人的总额不变
- 隔离性(Isolation):并发访问时,一个用户的事务不被其他事务干扰,各并发事务之间数据库是独立的
- 持久性(Durability):事务提交后,对数据库中数据的改变是持久的,即使数据库发生故障也不应受影响
核心要点
MySQL事务与并发控制,包括ACID特性、隔离级别、MVCC原理
并发事务带来的问题
- 脏读(Dirty read):一个事务读取到另一个事务尚未提交的数据修改
- 丢失修改(Lost to modify):两个事务同时读取同一数据并修改,导致其中一个修改被覆盖
- 不可重复读(Unrepeatable read):同一事务内多次读取同一数据,结果不一致
- 幻读(Phantom read):同一事务内多次查询,发现多出或少了记录
不可重复读的重点是修改(值变化),幻读的重点是新增或删除(记录数变化)。
事务隔离级别
SQL 标准定义了四个隔离级别:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ-UNCOMMITTED | √ | √ | √ |
| READ-COMMITTED | × | √ | √ |
| REPEATABLE-READ | × | × | √ |
| SERIALIZABLE | × | × | × |
√ = 该问题可能发生,× = 不会发生。隔离级别越高,并发度越低、性能开销越大。
- READ-UNCOMMITTED(读取未提交):最低级别,允许读取尚未提交的数据变更
- READ-COMMITTED(读取已提交):允许读取并发事务已提交的数据,可阻止脏读
- REPEATABLE-READ(可重复读):对同一字段多次读取结果一致,可阻止脏读和不可重复读
- SERIALIZABLE(可串行化):最高级别,完全服从 ACID,所有事务依次逐个执行
MySQL InnoDB 默认使用 REPEATABLE-READ。MVCC 保证的是快照读(普通 SELECT,读某个历史版本);而 SELECT ... FOR UPDATE、UPDATE、DELETE 属于当前读(必须读最新版本并加锁),此时 InnoDB 用 Next-Key Lock 锁住扫描区间,阻止其他事务插入新记录,从而在 RR 级别下实际避免了幻读——这也是"默认 RR 也不会出问题"的原因。
分布式事务场景一般使用 SERIALIZABLE 隔离级别。
MVCC 实现原理
Multi-Version Concurrency Control(多版本并发控制),实现依赖于三个核心组件:
隐藏字段
DB_TRX_ID:6字节,最近修改事务 IDDB_ROLL_PTR:7字节,回滚指针,指向这条记录的上个版本DB_ROW_ID:6字节,隐藏的主键
Undo Log
回滚日志,在 insert、delete、update 操作时产生,用于回滚到之前版本。事务执行中出错或主动回滚时,要把数据恢复到修改前的状态;MVCC 的快照读又需要"过去的版本"——这两件事都靠它。
Undo Log 对每条修改记录反向操作:INSERT 记删除该行、DELETE 记原始行、UPDATE 记更新前的旧值,回滚时按反向操作逐个执行即可还原;多个历史版本通过回滚指针(DB_ROLL_PTR)串成版本链,供 MVCC 按 Read View 挑选可见版本。
历史版本不能无限保留,purge 线程会在无事务引用后清理;长事务会阻塞清理,导致 undo 膨胀、版本链变长、查询变慢。
Read View
三个全局属性:
trx_list:维护 Read View 生成时刻系统正活跃的事务 ID 列表up_limit_id:trx_list 中事务 ID 最小的值low_limit_id:Read View 生成时刻系统尚未分配的下一个事务 ID
可见性判断规则
用记录的 DB_TRX_ID 与 Read View 比较,决定读到哪个版本:
trx_id < up_limit_id:该版本的事务在 Read View 生成前就已提交,可见trx_id >= low_limit_id:该版本的事务在 Read View 生成之后才开始,不可见up_limit_id <= trx_id < low_limit_id:查 trx_id 是否在trx_list中——在则事务仍活跃未提交,不可见,沿DB_ROLL_PTR回滚指针找上一个版本再判断;不在则事务已提交,可见
同一事务的多个快照读复用同一个 Read View,因此看到的结果始终一致,这就是可重复读的实现。READ-COMMITTED 级别下每次 SELECT 都新建 Read View,所以只能保证"读到已提交",不能保证两次读取一致。
快照读 vs 当前读
| 类型 | 读什么版本 | 是否加锁 | 典型语句 |
|---|---|---|---|
| 快照读 | 历史版本(走 MVCC) | 不加锁,不阻塞 | 普通 SELECT |
| 当前读 | 最新已提交版本 | 加锁,阻塞冲突写 | SELECT ... FOR UPDATE、UPDATE、DELETE、INSERT |
MVCC 只服务于快照读;当前读依赖锁机制(记录锁 + 间隙锁组成的 Next-Key Lock)保证并发安全。