4. 事务与ACID:事务概念、原子性、一致性、隔离性、持久性、事务日志的作用
聊数据完整性,绕不开事务。我见过不少开发同学,写代码时把事务当成「要么全成功,要么全失败」的开关,但真到了生产环境,死锁、回滚失败、数据不一致,往往就是因为对事务的理解只停留在表面。
这一章,咱们把事务的底裤扒干净。从概念到ACID,再到事务日志的物理实现,我会结合自己踩过的坑,帮你建立起对事务的完整认知。
4.1 事务是什么?
事务,说白了就是一组数据库操作,要么全部执行成功,要么全部不执行。你想想看,银行转账:A扣100,B加100。如果扣完钱系统崩了,B没加上,这账就乱了。事务就是用来保证这种场景不出问题的。
事务的经典定义: 事务是数据库管理系统执行过程中的一个逻辑单位,由一个或多个SQL语句组成,这些语句作为一个整体提交或回滚。
我个人习惯把事务看作一个「工作单元」。这个单元里的操作,数据库必须保证它们要么全部生效,要么全部不生效,没有中间状态。
4.2 ACID四大特性
ACID是事务的四个核心特性。我当年刚入行时,觉得这四个词背下来就行。直到有一次线上事故,因为隔离级别没选对,导致数据错乱,我才真正理解了它们的重量。
| 特性 | 英文 | 一句话解释 |
|---|---|---|
| 原子性 | Atomicity | 事务里的操作,要么全做,要么全不做 |
| 一致性 | Consistency | 事务执行前后,数据必须符合所有约束 |
| 隔离性 | Isolation | 并发事务之间互不干扰 |
| 持久性 | Durability | 事务一旦提交,数据永久保存 |
4.2.1 原子性(Atomicity)
原子性是最直观的。一个事务里的所有操作,要么全部成功,要么全部回滚。不能出现「一半成功一半失败」的情况。
我记得有一次,一个同事写了个批量插入的存储过程,中间某条数据违反唯一约束,结果前面的插入了,后面的没插入。这就是原子性没保证好。后来我们加上了事务包裹,用SAVEPOINT做了细粒度控制。
我的经验: 原子性靠的是事务日志中的undo信息。回滚时,数据库用undo日志把已执行的操作「撤销」掉。所以,原子性不是魔法,是日志撑起来的。
4.2.2 一致性(Consistency)
一致性是ACID里最容易被误解的。很多人以为一致性就是数据不丢,其实不止。一致性要求事务执行前后,数据库必须满足所有预定义的规则——主键约束、外键约束、唯一约束、检查约束、触发器逻辑等等。
举个例子:你定义了一个字段age必须大于0。一个事务试图把age改成-5,就算事务提交了,数据库也必须拒绝。这就是一致性在起作用。
我曾经在项目中遇到过一个问题:一个批量更新脚本,因为忘记检查外键约束,导致子表出现了「孤儿记录」。从那以后,我养成了一个习惯——任何涉及多表关联的更新操作,先检查约束,再开事务。
注意: 一致性是应用程序和数据库共同的责任。数据库保证约束,应用程序保证业务逻辑。别指望数据库能替你检查所有业务规则。
4.2.3 隔离性(Isolation)
隔离性解决的是并发问题。多个事务同时操作同一份数据,怎么保证不出乱子?数据库通过锁机制和MVCC(多版本并发控制)来实现隔离。
隔离性有四个级别,从低到高分别是:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交(Read Uncommitted) | 可能 | 可能 | 可能 |
| 读已提交(Read Committed) | 不可能 | 可能 | 可能 |
| 可重复读(Repeatable Read) | 不可能 | 不可能 | 可能 |
| 可串行化(Serializable) | 不可能 | 不可能 | 不可能 |
我建议,生产环境至少用读已提交。MySQL的InnoDB默认是可重复读,但很多场景下读已提交就够用了,性能还更好。我曾经把一个报表系统的隔离级别从可重复读降到读已提交,查询响应时间直接降了40%。
4.2.4 持久性(Durability)
持久性保证:一旦事务提交成功,数据就不会丢失。哪怕下一秒数据库崩溃、机房断电,数据也必须还在。
持久性靠的是事务日志(redo log)。数据先写日志,再写数据文件。日志写成功了,事务才算提交。崩溃恢复时,数据库重放redo log,把已提交但还没写入数据文件的操作重新执行一遍。
关键点: 持久性不等于「数据立刻落盘」。很多数据库允许延迟写入数据文件,但日志必须同步刷盘。这就是所谓的WAL(Write-Ahead Logging)机制。
4.3 事务日志的作用
事务日志是数据库的「黑匣子」。它记录了事务的每一个操作步骤。没有事务日志,ACID就是空中楼阁。
事务日志主要有三种:
- Undo Log(回滚日志): 记录修改前的数据,用于事务回滚和MVCC。
- Redo Log(重做日志): 记录修改后的数据,用于崩溃恢复。
- Binlog(二进制日志): 记录所有数据变更,用于主从复制和时间点恢复。
我画了一张图,帮你理清事务日志在事务执行过程中的位置:
从这张图你能看到,事务执行过程中,日志是优先于数据写入的。这就是WAL的核心思想——先写日志,再写数据。崩溃恢复时,数据库通过redo log重放已提交的事务,通过undo log回滚未提交的事务。
4.4 事务的常见问题与避坑
讲完理论,说点实战中容易踩的坑。
4.4.1 长事务的危害
长事务是数据库的隐形杀手。一个事务长时间不提交,会带来三个问题:
- 锁持有时间过长: 阻塞其他事务,降低并发能力。
- Undo Log膨胀: 长事务需要保留旧版本数据,undo日志无法清理,导致磁盘空间暴涨。
- 死锁风险增加: 事务持有锁的时间越长,与其他事务产生死锁的概率越高。
我曾经接手过一个系统,数据库磁盘使用率飙到95%。查了半天,发现是一个后台任务开了事务,循环处理数据,每处理一条就sleep一秒,事务一直不提交。undo日志撑爆了磁盘。解决方案很简单:每处理一批数据就提交一次事务。
我的建议: 事务要短、要快。能用一条SQL搞定的,别拆成多条。必须分批处理的,每批单独提交。监控中如果发现长事务,立刻报警。
4.4.2 隔离级别选择不当
隔离级别不是越高越好。可串行化虽然最安全,但性能最差。我见过有人把所有事务都设成可串行化,结果系统并发上不去,频繁超时。
我的经验是:
- 大部分业务场景,读已提交就够用。
- 需要保证同一事务内多次读取结果一致,用可重复读。
- 只有极少数对数据一致性要求极其严格的场景(比如金融对账),才考虑可串行化。
4.4.3 事务日志的监控
事务日志是数据库健康的重要指标。我建议你关注以下几点:
| 监控项 | 正常范围 | 异常信号 |
|---|---|---|
| Redo Log写入速率 | 平稳波动 | 突然飙升(可能有大事务) |
| Undo Log空间使用率 | < 70% | > 90%(可能有长事务) |
| 事务平均执行时间 | < 100ms | > 1s(需要优化) |
| 死锁发生频率 | 0次/天 | 出现即需排查 |
警告: 不要忽视事务日志的监控。我见过一个案例,redo log文件配置太小,业务高峰期日志切换频繁,导致数据库性能断崖式下跌。事后复盘,如果提前监控了redo log的切换频率,完全可以避免。
4.5 小结
事务和ACID是数据库数据完整性的基石。原子性靠undo log,持久性靠redo log,一致性靠约束和业务逻辑,隔离性靠锁和MVCC。事务日志是这一切的幕后英雄。
记住一句话:没有事务,就没有数据完整性。 写代码时,把事务当回事;运维时,把日志当回事。这两件事做好了,数据出问题的概率能降低80%。