当鸣人掏出20两银子购买拉面时,系统需要同时完成“扣钱”和“记账”两个操作。
如果其中一步执行失败,就可能出现钱没了、面没吃到的离谱事故。为了避免这种情况,数据库设计出了事务(Transaction)这一“S级忍术”,通过 ACID 四大特性保证数据操作的安全与一致。
本文将跟随鸣人的脚步,从一碗拉面出发,深入剖析事务的诞生原因、ACID 的核心思想以及 MySQL 的底层实现机制,带你彻底掌握数据库事务这一面试与实战中的高频知识点。
为什么要有事务?

核心痛点场景:
鸣人身上有 100 两银子,买一碗拉面需要 20 两。
整个购买过程实际上包含两步操作:
如果第一步执行成功了,鸣人的钱被扣了;但第二步因为系统故障、网络异常等原因失败了,导致拉面馆账本没有记录这笔收入。
此时就会出现问题:
- 鸣人的钱少了 20 两;
- 拉面馆却没收到这 20 两;
- 整个系统的数据出现了不一致。
为了解决这类问题,数据库引入了事务机制。
事务是什么?
定义:事务是一组 SQL 构成的不可分割的逻辑整体。
核心规则:要么全部执行成功,要么全部失败回滚,永远不会停在部分成功部分失败的中间状态。
事务的四大特性

原子性(Atomicity)
- 定义:事务是最小执行单元,要么全成功,要么全失败,不存在中间态。
- 解决的问题:就是我们刚才转账场景的核心诉求:绝对不能出现「A 扣了钱,B 没加钱」的中间状态。
-
底层实现:靠undo log(回滚日志)实现:事务执行的每一步修改,都会在 undo log 里记录对应的反向操作(比如「扣 100 元」就记录「加 100 元」),如果任意一步执行失败,就通过 undo log 把之前的所有操作全部撤销,回到初始状态。
持久性(Durability)
-
定义:事务一旦提交,对数据的修改就是永久的,哪怕数据库崩溃,修改也不会丢失。
-
解决的问题: 原子性解决了「执行中失败回滚」的问题,那如果事务已经提交成功了,数据库突然崩溃断电,不能让刚提交的数据丢失。
-
底层实现:通过redo log(重做日志)实现。事务提交时先把修改写入 redo log 持久化到磁盘,哪怕 MySQL 内存数据没刷到磁盘,崩溃后也能通过 redo log 恢复数据。
一致性(Consistency)
-
定义:事务执行前后,数据的业务逻辑保持一致(比如转账前后,两人的总金额永远不变)。
隔离性(Isolation)
有了原子性和持久性,单事务执行的问题解决了。
但实际业务中,数据库永远是多个事务并发执行的:几百个用户同时下单、同时转账,如果多个事务互相干扰,就会出现各种诡异的数据异常。
隔离性就是用来解决并发事务互相干扰的问题,保证多个事务独立执行,互不影响。
隔离性要解决的 3 类并发异常
如果没有隔离性,并发事务会出现 3 种严重的数据问题。

脏读:读到了别人没提交的临时数据
业务例子:商品库存本来是 0,事务 A 把库存改成 1,还没提交;事务 B 查询库存读到了 1,开始执行扣库存的逻辑。结果事务 A 因为其他逻辑失败回滚了,库存又变回 0,事务 B 扣库存就会出现超卖。
→ 核心问题:读到了其他事务未提交的临时数据,这些数据随时可能被回滚。
不可重复读:同一个事务里,同一个数据读两次不一样
业务例子:事务 A 查询用户余额是 100 元,准备做扣款;此时事务 B 给这个用户转了 200 元并提交;事务 A 第二次查询余额,发现变成了 300 元,同一个事务里两次读到的余额不一样,业务逻辑直接乱掉。
→ 核心问题:同一个事务内,其他事务的提交修改对当前事务可见,导致前后读取结果不一致。
幻读:同一个事务里,范围查询的条数不一样
业务例子:事务 A 统计 1 月份的订单,第一次查询有 100 条;此时事务 B 新增了一条 1 月份的订单并提交;事务 A 第二次统计,发现变成了 101 条,同一个事务里两次统计的订单数不一样,报表数据直接出错。
核心区分:不可重复读是单条数据的值变了,幻读是查询到的数据条数变了,像出现了幻觉一样。
问题解决:通过事务的隔离级别来解决。
事务的隔离级别:
我们知道了并发会带来 3 类数据异常,那怎么解决这些问题?数据库给我们提供了四种隔离级别,本质是数据一致性和并发性能的权衡:隔离级别越高,数据越安全,但并发性能越差。
四个隔离级别,从低到高
| 读未提交 | 什么都解决不了 | 能读到其他事务所有未提交的修改 | 完全不用,性能最高但数据完全不可靠 |
| 读已提交 | 解决脏读 | 只能读到其他事务已经提交的修改 | Oracle、PostgreSQL 的默认级别,适合对一致性要求不高的业务 |
| 可重复读 | 解决脏读、不可重复读 | 同一个事务内,永远读到第一次查询的快照数据 | MySQL 的默认级别,生产环境最常用,平衡性能和一致性 |
| 串行化 | 解决所有问题 | 事务完全串行执行,不允许并发 | 完全不用,性能极差,会导致大量锁等待和超时 |
串行化底层实现:直接加锁。
读已提交和可重复读的底层实现:MVCC
现在我们知道了隔离级别的规则,那 MySQL 底层是怎么实现读已提交和可重复读这两个常用隔离级别的?总不能靠加锁吧?全加锁的话并发性能就太差了。
这就引出了 MySQL 最核心的设计:MVCC 多版本并发控制—— 不用加锁,通过数据多版本实现读写不冲突,大大提升并发性能。
MVCC 的基础:数据的版本链
MySQL 的每一行数据,除了我们自己定义的字段,还有两个隐藏字段:
比如一行数据的初始值是 1,事务 1 把它改成 2,事务 2 把它改成 3,就会形成这样的版本链: 当前值3(事务2修改)→ 版本2(事务1修改)→ 版本1(初始值)
MVCC 的核心:ReadView(读快照)
有了版本链,那事务读的时候,该读哪个版本?靠ReadView来判断可见性:
每次执行 SELECT 查询的时候,会生成一个 ReadView,里面记录了当前所有「活跃的、未提交的事务 ID」,通过对比数据行的事务 ID 和 ReadView,就能判断这个版本的数据对当前事务是否可见:
- 如果数据的事务 ID 已经提交,就可见;
- 如果不可见,就顺着版本链往前找,直到找到可见的版本。
两个隔离级别的实现差异
读已提交和可重复读的唯一区别,就是「什么时候生成 ReadView」:
可重复读(MySQL 默认):整个事务生命周期中,只在第一次执行 SELECT 的时候生成一次 ReadView,后面所有查询都复用这个 ReadView → 所以同一个事务里,所有查询看到的版本永远是第一次的快照,不管其他事务怎么修改提交,都看不到,自然就解决了不可重复读。
读已提交:每次执行 SELECT,都会生成一个全新的 ReadView → 每次查询都能看到其他事务新提交的版本,所以能读到最新提交的数据,解决了脏读,但解决不了不可重复读。
整个事务的体系就是一个完整的「问题→解决方案」链条,没有任何零散的知识点:






