MySQL 事务怎么理解:ACID、隔离级别、MVCC 与幻读
MySQL 事务章节,讲清 ACID 四大特性、并发事务的脏读/不可重复读/幻读、四种隔离级别,以及 InnoDB 通过 MVCC 和 next-key lock 处理一致性问题的思路。
相关工具
文章目录(9 个章节)
事务不是把 SQL 包起来那么简单
很多人第一次写事务,是在代码里加上 `begin`、`commit`、`rollback`。这当然是入口,但如果只把事务理解成“多条 SQL 一起提交”,后面遇到并发修改、锁等待、长事务、幻读、回滚日志时,就会一下子散掉。
相关基础内容 的事务章节从一个很关键的事实讲起:MySQL 原生引擎 MyISAM 不支持事务,所以后来 InnoDB 成了更常用的选择。事务要解决的不是语法问题,而是业务正确性问题。转账时不能只扣 A 的钱却没给 B 加钱;订单创建失败时不能留下半截库存扣减;系统崩溃后,已经提交的修改不能凭空消失。
这一篇按一条主线讲:事务先用 ACID 定义正确性目标,再用隔离级别处理并发读写的边界,底层通过 undo log、redo log、MVCC 和锁来支撑。你先把这条线走通,再去背脏读、不可重复读、幻读,会轻松很多。

ACID 定义事务的正确性目标;隔离级别决定并发读写时能看到什么;MVCC 和锁负责落地实现。
ACID 是四个不同角度
ACID 分别是 Atomicity、Consistency、Isolation、Durability,也就是原子性、一致性、隔离性、持久性。文中对原子性的解释很直接:事务是一个不可分割的工作单元,要么全部执行,要么全部不执行。如果事务执行过程中发生错误,系统会撤销已经执行的操作,把数据库恢复到事务开始前的状态。原子性主要靠 undo log 保证。
一致性说的是,事务要把数据库从一个一致状态转到另一个一致状态。比如库存不能扣成负数,唯一键不能重复,外键关系不能破坏。文中提到,一致性是通过持久性、原子性、隔离性共同保证的。它更像最终目标,不是某一个单独组件能独立完成的事。
隔离性处理的是并发。多个事务同时执行时,一个事务不应该看到另一个事务的中间状态。文中说,隔离性通过 MVCC 或锁机制保证。持久性则要求事务一旦提交,结果永久保存在数据库里,即使系统故障也不应该丢失,主要靠 redo log 保证。
并发事务会带来三类典型问题
数据库如果一次只让一个事务执行,很多问题会消失,但性能也会很差。现实系统需要并发事务提升吞吐量。问题是,并发一出现,读和写就可能互相影响。文中列了三个常见现象:脏读、不可重复读、幻读。
脏读是读到了其他事务还没提交的数据。比如事务 B 修改了余额但还没提交,事务 A 先读到了这个新余额,随后事务 B 回滚。对事务 A 来说,它刚才读到的是一个最终并不存在的值。文中说,这就是读到了不一定最终存在的数据。
不可重复读是同一个事务里,前后两次读取同一行数据,结果不一致。原因通常是另一个事务在中间修改并提交了这行数据。幻读则看的是记录数量:一个事务内多次查询符合条件的记录,前后数量不一样,比如第一次查不到某个范围内的新订单,第二次查到了别人刚插入的订单。
四种隔离级别是在做取舍
SQL 标准定义了四种隔离级别。读未提交最低,在这个级别下,一个事务可以读到另一个事务未提交的数据,所以可能出现脏读、不可重复读和幻读。它并发能力强,但正确性边界很松,业务系统里一般不会随便使用。
读提交只允许读取已经提交的数据,因此解决了脏读。但它仍可能出现不可重复读和幻读,因为同一个事务里,每次查询都可能看到其他事务最新提交的结果。后文讲实现时提到,读提交下每个 `select` 都会生成新的 Read View,所以前后两次读取看到的版本可能不同。
可重复读要求一个事务生命周期内,多次执行相同查询能看到一致的数据。文中说,它也是 MySQL InnoDB 的默认隔离级别。串行化最高,会用读写锁让冲突事务排队,效果像事务按顺序执行,可以防止脏读、不可重复读和幻读,但并发性能会下降。隔离级别不是越高越好,而是业务正确性和并发性能之间的选择。
InnoDB 默认可重复读,但仍要理解幻读
文中有一句容易被误读的话:可重复读级别下仍可能发生幻读,但 MySQL InnoDB 在默认可重复读下,很大程度上可以避免幻读。关键在于读的类型不同。普通 `select` 属于快照读,`select ... for update`、`update`、`delete`、`insert` 等更接近当前读。
对快照读,InnoDB 通过 MVCC 解决。一个事务开始后,会基于 Read View 看到某个版本范围内的数据。即使其他事务后来插入或修改并提交,当前事务的普通查询仍可以按自己的 Read View 读取合适版本,避免前后结果乱跳。
对当前读,InnoDB 依赖 next-key lock,也就是记录锁加间隙锁。文中说,执行 `select ... for update` 时会加 next-key lock,如果其他事务想在锁范围内插入记录,就会被阻止。这样可以避免当前读场景下出现满足条件的新记录突然冒出来。
Read View 是 MVCC 的观察窗口
资料 对 Read View 的描述比较细。事务启动时,系统会为事务创建 Read View。它里面包含几个关键信息:当前活跃事务 ID 列表 `m_ids`,活跃事务中最小的事务 ID `min_trx_id`,下一个将要分配的事务 ID `max_trx_id`,以及创建这个 Read View 的事务 ID `creator_trx_id`。
聚簇索引记录里还有两个隐藏列:`trx_id` 和 `roll_pointer`。一条记录被事务修改时,会把该事务 ID 写到 `trx_id`;旧版本记录会被写入 undo 日志,`roll_pointer` 指向旧版本。这样一行数据就不只有“现在长什么样”,还可以沿着 undo log 找到历史版本。
读取时,InnoDB 会拿记录版本的 `trx_id` 和 Read View 做判断。如果记录版本在 Read View 创建前已经提交,当前事务可以看见;如果是 Read View 创建后才启动的事务生成的版本,通常看不见;如果事务 ID 落在活跃事务范围里,还要看它是否仍在 `m_ids` 中。这个判断过程,就是 MVCC 能做到不加锁也能读一致快照的基础。
读提交和可重复读的关键差别
读提交和可重复读都可以通过 MVCC 实现,但 Read View 的生成时机不同。文中说,读提交是在每个 `select` 都生成一个新的 Read View。也就是说,一个事务里第一次查询看到一个版本,第二次查询可能因为其他事务已经提交,而看到更新后的版本。
可重复读是在事务启动时生成一个 Read View,整个事务期间都使用这个 Read View。因此同一事务内多次读取同一批数据,通常能看到一致结果。你可以把读提交理解成“每次读都看当时已经提交的世界”,把可重复读理解成“事务开始时拍了一张快照,后面普通读取都按这张快照看”。
这个差别在排查问题时很有用。如果业务说“同一个事务里两次查询结果变了”,先看隔离级别是不是读提交。如果使用可重复读但仍出现类似幻读现象,再看是不是当前读、范围更新、锁范围或事务启动方式的问题。
长事务会把好机制拖成问题
资料 事务章节还提醒了长事务。事务可以显式启动,比如 `begin` 或 `start transaction`,最后 `commit` 或 `rollback`。也可以通过关闭自动提交 `set autocommit=0` 让后续语句都处在事务里。问题是,如果连接一直不断,这个事务可能持续很久。
长事务的风险不只是占着连接。它会让 Read View 长时间存在,旧版本数据迟迟不能清理,undo 日志压力增加,也可能让锁等待扩大。资料 建议使用 `set autocommit=1`,并从应用侧检查是否误用了 `set autocommit=0`、是否有不必要的只读事务、是否限制每条语句最长执行时间。
数据库侧也要监控长事务。已有内容提到可以在 `information_schema` 库下的 `innodb_trx` 表中查询长事务,也可以设置阈值报警或 kill。事务不是开得越大越安全。越靠近业务动作的最小边界,越容易控制锁、版本和回滚成本。
实际回答时抓住这条主线
如果面试或文档里要解释 MySQL 事务,不要只背 ACID 四个词。可以先说:事务保证一组操作要么一起成功,要么一起失败;原子性靠 undo log,持久性靠 redo log,隔离性靠 MVCC 或锁;一致性则是这些机制和业务约束一起达到的目标。
然后再讲并发问题:脏读是读到未提交数据,不可重复读是同一行前后结果不同,幻读是符合条件的记录数量前后不同。隔离级别从低到高是读未提交、读提交、可重复读、串行化。级别越高,并发冲突越少,但性能和等待成本也会上升。
最后把 InnoDB 的默认行为补上:默认隔离级别是可重复读,普通 select 通过 MVCC 和 Read View 读快照,当前读通过 next-key lock 处理范围内的并发插入。这样回答,既有概念,也有机制,不会停留在名词解释。
常见问题
MySQL 事务的 ACID 分别靠什么保证?
原子性主要靠 undo log,隔离性靠 MVCC 或锁,持久性靠 redo log,一致性则依赖原子性、隔离性、持久性以及数据库约束共同保证。
脏读、不可重复读、幻读有什么区别?
脏读是读到未提交数据;不可重复读是同一事务内两次读同一行,结果不同;幻读是同一事务内两次按条件查询,符合条件的记录数量不同。
读提交和可重复读的主要区别是什么?
读提交通常每次 select 都生成新的 Read View,所以能看到其他事务后来提交的数据;可重复读在事务启动时生成 Read View,整个事务期间普通读取都基于同一份快照。
InnoDB 怎么避免幻读?
普通 select 这类快照读通过 MVCC 读取一致版本;当前读,如 select ... for update,通过 next-key lock,也就是记录锁加间隙锁,阻止其他事务在锁范围内插入满足条件的新记录。