事务基础回顾:从ACID到MVCC,那些年我们踩过的坑
各位同学,咱们今天聊点基础但极其重要的东西——事务。你可能觉得这玩意儿太简单了,不就是ACID嘛。但说实话,我在金融系统里摸爬滚打这么多年,发现很多线上事故的根源,恰恰是这些“基础”没吃透。
好,咱们开始。今天的内容,我打算这么展开:先聊聊本地事务的ACID到底在说什么,然后看看MySQL的事务隔离级别怎么选,接着深入MVCC的原理,最后说说事务并发到底会带来哪些问题。
1. ACID:不只是四个字母
ACID这四个字母,背下来很容易。但你真的理解它们在实际系统中意味着什么吗?
| 特性 | 含义 | 我踩过的坑 |
|---|---|---|
| 原子性(Atomicity) | 事务要么全做,要么全不做 | 曾经有个同事在转账逻辑里,扣款成功但加款失败,结果钱凭空消失了…… |
| 一致性(Consistency) | 事务前后,数据约束不被破坏 | 账户余额不能为负,这个约束如果没在事务里保证,后果你懂的 |
| 隔离性(Isolation) | 并发事务互不干扰 | 这个最头疼,后面细说 |
| 持久性(Durability) | 事务提交后,数据永久保存 | 别以为刷盘就万事大吉,主从延迟了解一下 |
我个人习惯把ACID理解成一句话:事务就是让数据库从一个一致状态,变到另一个一致状态,中间不管出什么幺蛾子,都不能留下烂摊子。
核心要点:原子性是手段,一致性是目的,隔离性是并发时的保障,持久性是最后的兜底。
2. MySQL事务隔离级别:选错了真要命
MySQL的InnoDB引擎支持四种隔离级别。我建议你记住这张表:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | ✅ 可能 | ✅ 可能 | ✅ 可能 |
| READ COMMITTED | ❌ 不会 | ✅ 可能 | ✅ 可能 |
| REPEATABLE READ | ❌ 不会 | ❌ 不会 | ✅ 可能(InnoDB通过间隙锁解决) |
| SERIALIZABLE | ❌ 不会 | ❌ 不会 | ❌ 不会 |
嗯,这里要注意。MySQL默认是REPEATABLE READ,但很多互联网公司会改成READ COMMITTED。为什么?
我在项目中遇到过这样一个场景:一个统计报表查询跑了5秒,期间有新的订单插入。如果用REPEATABLE READ,这个查询看到的是事务开始时的快照,新订单看不到。但业务方说“我要看到最新的数据啊”。最后我们改成了READ COMMITTED,每次查询都读最新已提交的数据。
我的建议:金融交易类系统,如果对一致性要求极高,用REPEATABLE READ甚至SERIALIZABLE。但要做好性能下降的心理准备。如果追求吞吐量,READ COMMITTED通常够用。
3. MVCC原理与快照读:InnoDB的杀手锏
MVCC,全称多版本并发控制。说白了,就是每个事务看到的数据版本可能不一样。
它是怎么实现的?核心靠三个东西:
- 隐藏字段:每行数据都有DB_TRX_ID(事务ID)、DB_ROLL_PTR(回滚指针)
- Undo Log:记录数据的历史版本
- Read View:事务启动时生成的一个“快照”,决定你能看到哪些版本
我画了一张图,帮你理解MVCC的核心逻辑:
快照读,就是普通的SELECT语句(不加FOR UPDATE或LOCK IN SHARE MODE)。它读的是事务开始时的快照,不会加锁。所以性能很好。
但要注意:快照读不是万能的。如果你需要“当前读”(比如SELECT ... FOR UPDATE),MVCC就帮不了你了,得靠锁。
我曾经踩过的坑:在一个高并发扣库存的场景里,我用了快照读来判断库存是否足够。结果两个事务同时读到库存为1,都以为够扣,最后库存变成了-1。嗯,从那以后,扣库存我必用SELECT ... FOR UPDATE。
4. 事务并发带来的问题:四个经典场景
事务并发,说白了就是多个事务同时操作同一份数据。这时候问题就来了:
- 脏读:读到别人还没提交的数据。万一人家回滚了,你读到的就是“脏”的。
- 不可重复读:同一个事务里,两次读同一行数据,结果不一样。因为中间被别的事务修改并提交了。
- 幻读:同一个事务里,两次查询同一范围的数据,结果行数不一样。因为中间有别的事务插入了新行。
- 丢失更新:两个事务同时读同一行数据,各自修改后提交,后提交的覆盖了先提交的。
你想想看,在交易系统里,这些问题哪个不是致命的?
举个例子:一个账户余额1000元。事务A要扣500,事务B要扣600。如果发生丢失更新,可能两个事务都读到1000,各自扣完提交,最后余额变成500或400,但实际应该扣1100,余额应为-100(如果允许透支的话)。
解决方案:
- 脏读 → 隔离级别至少READ COMMITTED
- 不可重复读 → 隔离级别至少REPEATABLE READ
- 幻读 → REPEATABLE READ + 间隙锁,或SERIALIZABLE
- 丢失更新 → 乐观锁(版本号)或悲观锁(SELECT ... FOR UPDATE)
好了,今天的基础就聊到这儿。这些概念虽然基础,但它们是后面所有高级话题的基石。下一章我们会深入分布式事务,到时候你会发现,今天讲的这些本地事务知识,全都能用上。