MySQL事务与隔离级别详解:脏读幻读复现与MVCC实践

17 ℃ 0 条
内容摘要MySQL 的隔离级别决定了并发事务之间能看到什么数据。本文用可直接执行的 SQL 复现脏读、不可重复读与幻读三类异常,讲清 Read View 与 undo log 如何支撑 MVCC,并给出可重复读与读已提交的选型建议和间隙锁注意事项。
MySQL事务与隔离级别详解:脏读幻读复现与MVCC实践

很多开发者能背出事务四大特性,却说不清脏读、幻读如何发生,也解释不了可重复读下为何仍有幻读。本文用可直接执行的 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;。

关键配置提示

  1. 事务尽量短,提交前不要夹杂网络请求,否则持锁时间会被拉长。
  2. 可重复读下要警惕间隙锁带来的插入等待,必要时把范围条件收敛到唯一索引。
  3. 只读统计类查询可显式开启只读事务,减少锁开销。

发表评论 (已有0条评论)

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