【第17天】每天一个MySQL知识点,百日打怪升级
事务基础:ACID 特性——面试必问的第一题
大家好,我是一名拥有10年以上经验的DBA老兵,没有那多。
做这个系列,源于一个朴素的愿望:把踩过的坑、总结的经验系统化输出,希望能帮到刚入行或想进阶的兄弟们。
让我们开始今天的第17天内容。
背景引入
💡 两个人同时抢最后一个座位——数据库是怎么保证不卖超的?
说个场景。
你打开 App 买一张高铁票,从北京到上海,G1 次,只剩最后一张。你点下单的同时,另一个人也在点。
系统先查了一下:余票 1 张。然后扣库存。
但如果你们俩的查询和扣减操作没有"保护"起来,就可能出现——两个人都查到余票 1 张,都成功扣了库存,结果变成 -1 张。后台一对账,发现卖超了。
这就是没有事务的后果。
事务就是给一组操作加上一个"要么全做,要么全不做"的边界。MySQL 里写 BEGIN 和 COMMIT,就是告诉数据库:这中间的所有操作,必须当成一个整体来处理。
今天的目标:搞懂事务的四个核心特性 ACID,以及 InnoDB 是怎么实现它们的。
核心概念
什么是事务?
事务就是一个不可分割的工作单元。你把它理解为数据库里的"原子操作"——里面的步骤要么全部成功,要么全部回滚,不存在"做了一半"的状态。
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
COMMIT;
转账 100 块,A 扣钱和 B 加钱必须同时成功。如果 A 扣完了,B 加钱之前数据库崩了——钱就丢了。事务就是来杜绝这种事的。
ACID 拆开讲
A——Atomicity(原子性)
要么全做,要么全不做。
InnoDB 靠 Undo Log 来实现。事务内的每一条写操作,都会同时记一条"怎么改回来"的日志在 Undo Log 里(存在系统表空间或独立的 Undo 表空间中,本身也是持久化的)。如果事务要回滚,InnoDB 就按 Undo Log 倒着重放,把数据恢复到事务开始前的样子。
面试常问的一个点:Undo Log 不只是用来回滚的,它还服务于 MVCC 的快照读(明天会细讲)。一条日志,两个用途。
C——Consistency(一致性)
事务执行前后,数据必须满足所有约束和业务规则。
扣了 A 的钱,B 必须多同样多的钱。余额不能为负。库存不能为负。
一致性其实是 ACID 里最"不易察觉"的一个。它不完全靠数据库——约束(主键、唯一键、外键、CHECK)是数据库保证的,但业务规则(比如"余额不小于 0")通常靠应用代码来实现。
MySQL 的一个常见"不一致"场景:用 UPDATE ... SET balance = balance - 100,如果 balance 是负数但没加 CHECK 约束,数据库不会拦你。一致性是业务和数据库共同的责任。
I——Isolation(隔离性)
多个事务同时执行,彼此不能干扰。
这个昨天已经讲了不少了——锁、隔离级别、MVCC,都是为隔离性服务的。隔离性越强,数据越安全,但并发越低。
MySQL 的默认隔离级别 REPEATABLE READ(RR)隔离性相当强,代价就是前面的 Gap Lock。如果你关注性能多于绝对一致性,READ COMMITTED(RC)通常是更好的选择。很多互联网公司默认就用 RC,配合 ROW 格式 binlog。
D——Durability(持久性)
事务提交后,数据必须永久保存,即使系统崩溃也不丢。
InnoDB 靠 Redo Log 来实现。它的做法是 WAL(Write-Ahead Logging):先写日志,再写数据。
事务提交时,不是直接把数据页写回磁盘(那太慢了),而是把修改记录写到 Redo Log,然后告诉客户端"事务成功了"。
Redo Log 是顺序写,磁盘性能很好。万一断电重启,InnoDB 的崩溃恢复分两步:
- REDO 阶段——扫 Redo Log,重放所有已提交但还没刷到数据页的修改,保证持久性
- UNDO 阶段——扫 Undo Log,回滚所有崩溃前没提交的事务,保证原子性
两步走完,数据库才回到一致状态。
这里有一个关键参数组合:
| 参数组合 | 安全性 | 性能 |
|---|---|---|
sync_binlog=1 + innodb_flush_log_at_trx_commit=1 |
最高(但最慢) | 每次提交都 fsync |
sync_binlog=0 + innodb_flush_log_at_trx_commit=2 |
最多可能丢最近 1 秒内提交的事务 | 性能好很多 |
所谓的"双一"配置(两个参数都是 1)就是 DBA 最看重的数据安全红线。丢了数据,谁都赔不起。
实战案例
场景一:理解原子性的"反例"
-- 没有事务的转账
UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 此时 MySQL 崩了... 钱扣了但没加上
UPDATE account SET balance = balance + 100 WHERE id = 2;
有事务保护:
BEGIN;
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- 要么两个都成功,要么一个都不执行
实际开发中常见的问题是:代码里写了事务,但在 catch 块里忘了 ROLLBACK。事务会继续挂着,锁不释放,其他请求只能干等。如果连接池满了,整个服务就挂了。
场景二:隔离性不足时的并发问题
有次排查一个"库存显示为 0 但还能下单"的 bug,根因很简单——用的是 READ UNCOMMITTED 隔离级别。A 事务下单扣库存还没提交,B 事务读到了 A 未提交的库存(脏读),此时 A 回滚了,B 拿着一个"假"库存继续下单。
隔离级别不足的表现就是:你读到的数据可能是不存在的。
避坑指南
⚠️ 真实踩过的坑:
没 rollback 的事务最坑
- 业务逻辑异常了,catch 里只记日志没 rollback
- 事务一直挂着,连接不释放,最终连接池撑爆
- 代码里写事务时,
finally或catch里一定要有 rollback - 工程化建议:用 Spring 等框架时,优先依赖声明式事务(
@Transactional),它自动处理回滚,比手动commit/rollback安全得多
"双一"配置不能随便改
sync_binlog=0或innodb_flush_log_at_trx_commit != 1在非核心业务上可以接受- 但金融、订单等核心业务必须"双一"——丢了数据是要背责任的
一致性不是数据库一个人的事
- 数据库保证的是约束级别的一致性
- 业务规则的一致性(余额不为负、库存不超卖)需要应用代码配合
- 可以在数据库加
CHECK约束兜底:ALTER TABLE account ADD CHECK (balance >= 0) - ⚠️ 注意:MySQL 8.0.16 之前虽然支持
CHECK语法,但不会实际检查——加了等于没加。从 8.0.16 开始才真正强制检查。如果你还在用 5.7,一致性全靠应用自己保证
事务不要开太大
- 前面两天的文章说过,大事务导致锁范围大、binlog 大、复制延迟
- 一条事务里只放必要的操作,无关的查询和更新不要塞进去
思考题
🤔 互动时间:
假设一张表没有设置主键,InnoDB 的 Redo Log 还能保证持久性吗?
提示:想想没有主键时 InnoDB 用什么来定位行,Redo Log 记录的是数据页偏移量还是逻辑 SQL?COMMIT执行完但客户端没收到响应——这时候如果断电重启,数据到底在不在?
提示:COMMIT 的核心是刷 Redo Log,刷成功了数据就在,没刷成功就不在。关键在于 COMMIT 语句返回前 Redo Log 落盘了没有。为什么说「Undo Log 既服务于原子性,也服务于隔离性」?它跟 MVCC 是什么关系?
提示:MVCC 的快照读需要能回溯到事务开始前的数据版本——这些旧版本就存在 Undo Log 里。原子性用 Undo Log 回滚,隔离性用 Undo Log 构建历史版本链。
总结
🎯 面试考点
- 事务的 ACID:原子性靠 Undo Log、一致性靠约束+业务、隔离性靠锁+MVCC、持久性靠 Redo Log
- Undo Log 双重身份:原子性(回滚)+ MVCC(快照读)
- Redo Log 是 WAL(Write-Ahead Logging):先写日志,再写数据,崩溃后重放
- "双一"配置(
sync_binlog=1+innodb_flush_log_at_trx_commit=1)是数据安全红线 - 事务不是越大越好——大事务 = 大锁 + 大 binlog + 大延迟
今天就试一下:连上你的数据库,跑一下这两条命令,看看当前是什么值:
SHOW VARIABLES LIKE 'sync_binlog';
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
如果是双 1——你的数据很安全。如果不是——想一想,如果现在断电,你会丢多少数据?
下期预告:事务隔离级别详解 —— 面试必问!
有问题欢迎评论区交流,明天见!