事务与隔离级别
问题
什么是数据库事务?MySQL 的四种隔离级别分别解决了什么问题?
答案
一、事务的 ACID 特性
| 特性 | 含义 | MySQL 实现机制 |
|---|---|---|
| 原子性(Atomicity) | 事务要么全部执行,要么全部不执行 | undo log — 记录旧值,回滚时恢复 |
| 一致性(Consistency) | 事务前后数据库保持一致状态 | 由其他三个特性共同保证 |
| 隔离性(Isolation) | 并发事务互不干扰 | MVCC + 锁 |
| 持久性(Durability) | 事务提交后数据永久保存 | redo log — WAL 机制保证崩溃恢复 |
面试要点
ACID 四个特性的 实现机制 是面试常问点:
- A → undo log
- I → MVCC + 锁
- D → redo log
- C → 由 A + I + D 保证
二、并发事务的问题
| 问题 | 描述 | 示例 |
|---|---|---|
| 脏读 | 读到其他事务 未提交 的数据 | 事务 A 修改了数据但未提交,事务 B 读到了修改后的值,A 回滚后 B 读到的就是脏数据 |
| 不可重复读 | 同一事务内两次读取 同一行,结果不同 | 事务 A 读 id=1 的 name='张三',事务 B 修改 name='李四' 并提交,事务 A 再读变成了'李四' |
| 幻读 | 同一事务内两次查询 行数 不同 | 事务 A 查询 age>20 得到 5 行,事务 B 插入一行 age=25 并提交,事务 A 再查得到 6 行 |
区分不可重复读和幻读:
- 不可重复读:针对 同一行 数据的 修改(UPDATE)
- 幻读:针对 行数增减(INSERT/DELETE)
三、四种隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现方式 |
|---|---|---|---|---|
| 读未提交 (READ UNCOMMITTED) | ❌ 可能 | ❌ 可能 | ❌ 可能 | 无 MVCC,直接读最新数据 |
| 读已提交 (READ COMMITTED) | ✅ 解决 | ❌ 可能 | ❌ 可能 | MVCC:每条 SQL 生成新的 Read View |
| 可重复读 (REPEATABLE READ) | ✅ 解决 | ✅ 解决 | ⚠️ 部分解决 | MVCC:事务开始时生成 Read View |
| 串行化 (SERIALIZABLE) | ✅ 解决 | ✅ 解决 | ✅ 解决 | 读加共享锁,写加排他锁 |
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
MySQL 默认隔离级别
MySQL 默认使用 REPEATABLE READ(可重复读),通过 MVCC + Next-Key Lock 在很大程度上解决了幻读。而 Oracle/PostgreSQL 默认使用 READ COMMITTED。
四、InnoDB 在 RR 级别如何解决幻读
InnoDB 在 REPEATABLE READ 下通过两种机制配合解决幻读:
| 读类型 | 机制 | 说明 |
|---|---|---|
| 快照读(SELECT) | MVCC Read View | 始终读事务开始时的一致性快照 |
| 当前读(SELECT FOR UPDATE / INSERT / UPDATE / DELETE) | Next-Key Lock(行锁 + 间隙锁) | 锁定范围,阻止其他事务在范围内插入 |
-- 快照读:不加锁,读 MVCC 快照
SELECT * FROM users WHERE age > 20;
-- 当前读:加 Next-Key Lock
SELECT * FROM users WHERE age > 20 FOR UPDATE;
INSERT INTO users (name, age) VALUES ('新人', 25);
UPDATE users SET name = '新名' WHERE age > 20;
RR 下幻读仍可能发生的情况
-- 事务 A
BEGIN;
SELECT * FROM users WHERE id = 5; -- 不存在,返回空(快照读)
-- 事务 B
INSERT INTO users (id, name) VALUES (5, '张三');
COMMIT;
-- 事务 A(继续)
UPDATE users SET name = '李四' WHERE id = 5; -- 成功!(当前读能看到 B 的插入)
SELECT * FROM users WHERE id = 5; -- 看到 id=5(MVCC 版本链更新了)
COMMIT;
这种场景下,事务 A 第一次快照读看不到 id=5,但 UPDATE 是当前读能修改到 id=5 的行,之后再次 SELECT 就能看到了 — 这就是 RR 下幻读的边界情况。
五、事务的使用
-- 显式事务
BEGIN; -- 或 START TRANSACTION
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- 提交
-- 出错时回滚
ROLLBACK;
-- 保存点(部分回滚)
BEGIN;
SAVEPOINT sp1;
UPDATE ...;
SAVEPOINT sp2;
UPDATE ...;
ROLLBACK TO sp2; -- 只回滚到 sp2,sp1 的修改保留
COMMIT;
常见面试问题
Q1: READ COMMITTED 和 REPEATABLE READ 的 MVCC 区别?
答案:
核心区别在于 Read View 的生成时机:
- RC:每条 SELECT 语句都创建新的 Read View → 能看到其他事务已提交的修改
- RR:整个事务第一条 SELECT 时创建 Read View,后续复用 → 事务内始终看到同一个快照
详见 MVCC 章节。
Q2: 为什么大多数互联网公司建议用 RC 而不是默认的 RR?
答案:
- RC 锁粒度更小:不需要间隙锁(Gap Lock),并发度更高
- RC 下可以用
binlog_format=ROW:解决主从数据一致性问题 - RR 的间隙锁容易导致死锁:特别是在批量操作时
- RC 更符合直觉:总是读到最新已提交数据
Q3: 长事务有什么危害?
答案:
- undo log 膨胀:长事务持有 Read View,旧版本无法回收
- 锁持有时间长:阻塞其他事务
- 回滚代价大:写了大量 redo log 和 undo log
- 主从延迟:长事务提交时 binlog 一次性发送
排查长事务:
-- 查找超过 60 秒的事务
SELECT * FROM information_schema.innodb_trx
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60;