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(二进制日志): 记录所有数据变更,用于主从复制和时间点恢复。

我画了一张图,帮你理清事务日志在事务执行过程中的位置:

事务执行与日志写入流程 事务执行与日志写入流程 开始事务 执行SQL语句 写入Undo Log 写入Redo Log 修改内存数据页 提交事务 Redo Log刷盘(持久化) 说明:数据页的刷盘可以延迟,但Redo Log必须在事务提交前刷盘(WAL机制)

从这张图你能看到,事务执行过程中,日志是优先于数据写入的。这就是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%。