
很多开发者能背出事务四大特性,却说不清脏读、幻读如何发生,也解释不了可重复读下为何仍有幻读。本文用可直接执行的 SQL 复现三类异常,再落到 MVCC 原理与隔离级别选型。
一、事务并发问题的来源
1. ACID 中隔离性最难把握
原子性、一致性、隔离性、持久性中,隔离性直接决定并发表现。多个事务同时读写同一批数据时,若不加控制就会看到对方的中间状态。InnoDB 用行锁与多版本并发控制(MVCC)实现隔离。
2. 四种隔离级别
- READ UNCOMMITTED:可读取其他事务未提交的修改,生产环境基本不用。
- READ COMMITTED:只能读到已提交数据,Oracle、PostgreSQL 的默认级别。
- REPEATABLE READ:同一事务内多次读取结果一致,InnoDB 的默认级别。
- SERIALIZABLE:读写完全串行,读操作加共享锁,并发能力最差。
级别越高一致性越强、并发越弱。
二、三类并发异常的复现
1. 准备测试环境
CREATE TABLE account (id INT PRIMARY KEY, balance INT NOT NULL) ENGINE=InnoDB;
INSERT INTO account VALUES (1,1000),(2,1000);
2. 脏读与不可重复读
脏读指 B 读到 A 未提交的修改。把会话级别设为 READ UNCOMMITTED,A 更新但不提交,B 仍能看到未落地的余额:
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
UPDATE account SET balance = 800 WHERE id = 1; -- A 未提交
SELECT balance FROM account WHERE id = 1; -- B 读到 800
不可重复读发生在 READ COMMITTED 下:同一事务内两次相同查询,因其他事务提交修改而返回不同结果;改用 REPEATABLE READ 后两次结果一致。
3. 幻读与间隙锁
幻读强调结果集行数变化:B 先统计符合条件的行数,A 插入新行并提交,B 再统计时数量变多,就像凭空多出记录。
SELECT COUNT(*) FROM account WHERE id > 0; -- 2
INSERT INTO account VALUES (3,1000);
SELECT COUNT(*) FROM account WHERE id > 0; -- 3
InnoDB 在可重复读下对范围查询加间隙锁(Gap Lock),锁住区间阻止其他事务插入,可抑制大部分幻读;但快照读一致性仍依赖 MVCC。
三、MVCC 与隔离级别实践
1. Read View 与版本可见性
InnoDB 每行都带 DB_TRX_ID 与 DB_ROLL_PTR 两个隐藏列,分别标记最近修改该行的事务与回滚指针。快照读时 InnoDB 依据 Read View 判断可见性:只有事务 ID 小于当前活跃事务最小值的版本才可见,否则顺着回滚指针到 undo log 寻找更早版本,这正是可重复读无需加锁即可保持一致的原因。
2. 选型建议
多数业务用默认的 REPEATABLE READ 即可;需要贴近 Oracle 行为时改成 READ COMMITTED,可减少间隙锁等待。余额扣减等强一致场景则用显式加锁把关键行串行化,例如 SELECT ... FOR UPDATE。确认当前级别可用 SELECT @@transaction_isolation;。
关键配置提示
- 事务尽量短,提交前不要夹杂网络请求,否则持锁时间会被拉长。
- 可重复读下要警惕间隙锁带来的插入等待,必要时把范围条件收敛到唯一索引。
- 只读统计类查询可显式开启只读事务,减少锁开销。

快来评论,快来抢沙发吧~