Skip to content

MySQL 基础(安装/CRUD/事务概念)见《MySQL 快速入门》,Spring 事务注解见《@Transactional 详解:为什么能回滚》。这篇把事务并发控制讲透:隔离级别、三种并发问题、MVCC 原理、锁。

一、为什么需要隔离级别

两个事务同时操作同一条数据时,会出现三种并发问题(按危害从小到大):

并发问题现象类比
脏读读到别人未提交的数据(对方回滚了,你读到的是假的)看到草稿就当定稿
不可重复读同一事务内两次读同一行,值不一样(别人提交了 UPDATE)看两遍金额不一样
幻读同一事务内两次查询,行数不一样(别人 INSERT 了新行)数两遍人数不一样

隔离级别就是"用并发性能换数据安全"的旋钮——级别越高,防的越多,但并发越差。

二、四种隔离级别

MySQL 隔离级别 vs 并发问题

隔离级别脏读不可重复读幻读说明
读未提交(RU)最低,什么都不防(几乎不用)
读已提交(RC)Oracle 默认,并发高
可重复读(RR)靠锁防MySQL 默认,MVCC 实现
串行化(Serializable)全表锁,最安全最慢
sql
-- 查看/设置隔离级别(会话级)
SELECT @@transaction_isolation;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

注意:MySQL 的 RR 用 MVCC 实现(不是锁表),所以 RR 下并发依然不错;而 Oracle 的默认 RC 是因为它认为 RR 影响并发——这是 MySQL 和 Oracle 最大的设计差异之一

三、三种并发问题实跑演示(MySQL 8 实测)

用两个会话(S1/S2)演示"不可重复读":

sql
-- S1:开启事务
START TRANSACTION;
SELECT balance FROM user WHERE id = 1;   -- 读到 100

-- S2(另一个会话):修改并提交
UPDATE user SET balance = 200 WHERE id = 1;
COMMIT;

-- S1 再读(RC 下会读到 200,RR 下还是 100)
SELECT balance FROM user WHERE id = 1;
  • RC 下:S1 第二次读到 200(不可重复读
  • RR 下:S1 第二次还是 100(MVCC 快照保证可重复读)

四、MVCC 原理:可重复读怎么实现的

**MVCC(多版本并发控制)**是 InnoDB 实现隔离级别的核心机制,原理三层:

1. 数据行有多个版本(靠 undo log)

每次 UPDATE 不是覆盖,而是生成新版本,旧版本存 undo log——一行数据有多个版本链

版本3 (balance=200) ← 版本2 (balance=150) ← 版本1 (balance=100)
最新                                                       最旧

2. 每个事务有 ReadView(快照)

事务第一次读时,生成一个 ReadView(记录"哪些版本我可见")——事务开启时已提交的版本可见,之后的版本不可见。

RR vs RC 的区别就在 ReadView 的生成时机:

ReadView 生成时机效果
RC每次 SELECT 都重新生成能看到别人新提交的 → 不可重复读
RR事务第一次 SELECT 生成,之后复用一直看最初快照 → 可重复读

3. 读哪一版?按 ReadView 找版本链

查询时沿着版本链找第一个满足 ReadView 可见性的版本——这就是"快照读",不加锁、并发高

五、锁:幻读怎么防(Next-Key Lock)

MVCC 解决脏读/不可重复读(快照读);幻读需要锁(当前读 SELECT ... FOR UPDATE / UPDATE / INSERT 时):

作用场景
记录锁(Record Lock)锁一行UPDATE 具体行
间隙锁(Gap Lock)锁一段区间(不含行)防区间内插入新行
Next-Key Lock记录锁 + 间隙锁RR 下防止幻读的核心

幻读演示(当前读场景):

sql
-- S1:锁住 balance > 50 的所有行(间隙也被锁)
START TRANSACTION;
SELECT * FROM user WHERE balance > 50 FOR UPDATE;

-- S2:插入 balance=60 的新行 → 被间隙锁挡住,阻塞!
INSERT INTO user(name, balance) VALUES ('new', 60);
-- 直到 S1 提交才执行

六、事务隔离级别怎么选

场景推荐
多数业务(订单/用户/库存)RC(并发高,可重复读需求少)
需要事务内多次读结果一致(对账/报表)RR(MySQL 默认,省心)
严格串行(金额极端敏感)Serializable(很少用,性能差)

Spring 里设置@Transactional(isolation = Isolation.READ_COMMITTED)(见《@Transactional 详解》的传播行为/隔离级别参数)。

小结

  • 三种并发问题:脏读(未提交)、不可重复读(行变)、幻读(行数变)
  • 隔离级别:RU→RC→RR→Serializable,MySQL 默认 RR
  • MVCC:undo log 版本链 + ReadView 快照——RR 复用快照所以可重复读
  • 幻读靠间隙锁/Next-Key Lock(当前读时生效)
  • 选型:业务默认 RC(并发高),对账/报表用 RR

下一篇可以看《MySQL 索引详解》(B+Tree、聚簇索引、EXPLAIN——查询为什么快的另一半)。