一、ACID 特性
| 特性 | 含义 | 实现机制 |
|---|---|---|
| 原子性 Atomicity | 要么全成功,要么全失败 | undo log(回滚日志) |
| 一致性 Consistency | 事务前后数据满足所有约束 | 其他三个特性共同保证 |
| 隔离性 Isolation | 并发事务互不干扰 | MVCC + 锁 |
| 持久性 Durability | 提交后永久保存 | redo log(重做日志) |
注意:一致性是"目标",ACI 是"手段"。面试时不要说"一致性靠 XX 实现"。
二、隔离级别
四种级别
| 级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED(读未提交) | ✅ | ✅ | ✅ |
| READ COMMITTED(读已提交) | ❌ | ✅ | ✅ |
| REPEATABLE READ(可重复读) | ❌ | ❌ | ⚠️ |
| SERIALIZABLE(串行化) | ❌ | ❌ | ❌ |
InnoDB 默认 RR,其他数据库(Oracle、PostgreSQL)默认 RC。
脏读、不可重复读、幻读的区别
-- 初始数据:id=1, balance=100
| 脏读 | 不可重复读 | 幻读 | |
|---|---|---|---|
| 现象 | 读到其他事务未提交的修改 | 同一事务两次读到不同的值 | 同一事务两次读到不同的行数 |
| 示例 | A 改 100→200 未提交,B 读到 200 | A 第一次读 100,B 改后提交,A 第二次读 200 | A 第一次查 5 行,B 插入一行提交,A 第二次查 6 行 |
| 触发操作 | UPDATE(读未提交数据) | UPDATE(数据变了) | INSERT/DELETE(行数变了) |
一句话区分:脏读是读到别人没提交的,不可重复读是同一行值变了,幻读是行数变了。
RR 级别的幻读问题
InnoDB 的 RR 通过 MVCC + Next-Key Lock 大部分解决了幻读,但仍有特殊情况:
-- 事务 A
SELECT * FROM t WHERE id > 10; -- 快照读,结果 2 行
-- 事务 B 插入 id=15 并提交
SELECT * FROM t WHERE id > 10; -- 快照读,仍 2 行(MVCC 起作用)
UPDATE t SET v = 1 WHERE id = 15; -- 更新了 B 刚插入的行!
SELECT * FROM t WHERE id > 10; -- 结果 3 行(幻读出现!)
原因:UPDATE 是当前读,触发了最新版本的更新,打破了快照的一致性。
三、MVCC 原理
MVCC(Multi-Version Concurrency Control)让 InnoDB 实现读不加锁、读写不冲突。
三个隐藏字段
每行记录都有:
| 字段 | 大小 | 作用 |
|---|---|---|
| DB_TRX_ID | 6 字节 | 最近修改这行的事务 ID |
| DB_ROLL_PTR | 7 字节 | 回滚指针,指向 undo log |
| DB_ROW_ID | 6 字节 | 行 ID(无主键时用) |
undo log 版本链
当前记录 → [trx_id=300, roll_ptr] → [trx_id=200, roll_ptr] → [trx_id=100, roll_ptr] → 初始版本
每次 UPDATE 不直接覆盖,而是在 undo log 中记录旧版本,形成链式版本。
ReadView
事务在快照读时创建一个 ReadView,包含四个关键信息:
creator_trx_id:创建 ReadView 的事务 IDm_ids:生成 ReadView 时活跃(未提交)的事务 ID 列表min_trx_id:m_ids 中最小的 IDmax_trx_id:下一个即将分配的事务 ID
可见性判断(从版本链最新往前找):
trx_id == creator_trx_id→ 可见(自己改的)trx_id < min_trx_id→ 可见(修改已提交)trx_id >= max_trx_id→ 不可见(修改在 ReadView 之后产生)trx_id in m_ids→ 不可见(修改未提交),不在则可见
RC 与 RR 的 ReadView 差异
| RC | RR | |
|---|---|---|
| 时机 | 每次 SELECT 生成新 ReadView | 事务内第一次 SELECT 生成,后续复用 |
| 效果 | 能看到其他事务已提交的修改 | 事务内一直看到相同快照 |
这就是为什么 RC 会有不可重复读而 RR 不会。
四、redo log 与 undo log
redo log(重做日志)
- 作用:保证持久性(崩溃恢复)
- 记录内容:物理日志,"对某表空间某页偏移量 N 写入 M"
- 写入方式:顺序写(append only),性能极高
- 关键参数:
innodb_log_buffer_size:日志缓冲区大小innodb_flush_log_at_trx_commit:- 0:每秒刷盘(可能丢 1 秒数据)
- 1:每次提交刷盘(默认,最安全)
- 2:每次提交写 OS 缓存,每秒刷盘(丢一个事务)
undo log(回滚日志)
- 作用:保证原子性(回滚)+ MVCC(读历史版本)
- 记录内容:逻辑日志,INSERT 对应 DELETE,UPDATE 对应逆向 UPDATE
- 存储位置:系统表空间或独立 undo 表空间
WAL(Write-Ahead Log)
redo log 的核心思想:先写日志再写磁盘,避免每次修改都随机写磁盘。
修改数据 → 写到 Buffer Pool → 写 redo log → 后台刷脏页 → 返回提交成功
崩溃恢复时重放 redo log 即可找回未刷脏页的数据。
五、事务的启动方式
显式事务
START TRANSACTION; -- 或 BEGIN
-- DML 操作
COMMIT; -- 或 ROLLBACK
隐式事务
SET autocommit = ON; -- 每条语句自动提交(MySQL 默认)
链式事务
SET autocommit = 0; -- 关闭自动提交,需手动 COMMIT
建议:长事务用 SET autocommit = 0 前确保及时提交,避免锁长期不释放。
六、长事务的危害与处理
危害
- undo log 膨胀:长事务意味着旧版本无法回收
- 锁长时间不释放:阻塞其他事务
- 主从延迟:大事务提交瞬间写大量 binlog
排查
-- 查找持续时间超过 60 秒的事务
SELECT * FROM information_schema.INNODB_TRX
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60;
避免
SET autocommit = ON(推荐,避免忘记提交)- 拆分大事务为小事务
- 避免在事务中做 RPC 调用等耗时操作
七、常见面试题
ACID 分别怎么实现的? → 原子性 undo log,一致性 ACI 共同保证,隔离性 MVCC+锁,持久性 redo log
RR 和 RC 区别? → RR 解决不可重复读(ReadView 复用),有间隙锁;RC 每次新 ReadView,无间隙锁
快照读和当前读? → 普通 SELECT 快照读(MVCC 无锁),FOR UPDATE/UPDATE/DELETE/INSERT 当前读(加锁)
MVCC 能完全解决幻读吗? → 不能,当前读场景仍可能幻读,需配合 Next-Key Lock
redo log 和 binlog 区别? → redo log 是 InnoDB 引擎层,物理日志,循环写;binlog 是 Server 层,逻辑日志,追加写
两阶段提交是什么? → redo log 先 prepare → 写 binlog → redo log commit,保证两者一致性
为什么 RR 没有完全解决幻读却被设为默认级别? → MVCC + Next-Key Lock 解决了绝大多数场景;配合 statement-based binlog 时 RR 比 RC 更安全