跳到主要内容

创建日期:2026-09-08 | 最近更新:2026-09-08 本机 MySQL 8.4.0 实测(两个并发会话演示隔离级别差异),输出真实。

MySQL 深潜 4:事务与隔离级别——并发下数据怎么「不错乱」

转账、下单这类「好几步操作要一起成功/一起失败」的场景,靠事务保证。事务是 MySQL 8 里 InnoDB 的看家本领,也是「初级 → 进阶」的分水岭。这篇用两个并发会话实测把 ACID 和隔离级别讲透。

1. ACID:事务的四个承诺

字母含义大白话
AAtomicity 原子性要么全做,要么全不做
CConsistency 一致性做完后数据仍符合规则
IIsolation 隔离性并发事务互不干扰(有级别之分)
DDurability 持久性提交后不丢(靠日志+崩溃恢复)

2. 怎么用:BEGIN / COMMIT / ROLLBACK

默认 MySQL 每条语句自动提交(autocommit=1)。要手动控制就开事务:

START TRANSACTION; -- 或 BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- 两步一起生效
-- 如果第二步出错:
-- ROLLBACK; -- 全部撤销,回到事务开始前

SAVEPOINT a; 可以在事务里设回滚点ROLLBACK TO a; 只回滚到某点而不放弃整个事务——大事务里局部纠错用。

查当前隔离级别:SELECT @@transaction_isolation;(默认 REPEATABLE-READ,见 §4)。

3. 先看结果:同样的流程,两个隔离级别的表现不同(实测)

准备一个订单 id=1,金额初始 21.37。会话 A 开启事务后先查一次,睡 5 秒再查一次;期间会话 B 把金额 +100 并提交。看 A 两次读到什么:

REPEATABLE-READ(MySQL 默认)

会话 A 第一次读 = 21.37;B 提交(变成 121.37)后,A 第二次读仍是 21.37

rr_before 21.37 ← A 事务开始时看到的值
rr_after 21.37 ← B 已提交 100,但 A 还是看到旧的

结论:同一事务内多次读,结果一致(可重复读)——A 的事务一开始就拍了一张「快照」,之后都读快照。

READ COMMITTED

同样的流程:A 第一次读 = 121.37(此时 B 上一轮已 +100);A 睡 5 秒期间 B 又 +100 并提交;A 第二次读 = 221.37

rc_before 121.37 ← 事务开始时的值
rc_after 221.37 ← 期间别人提交了,A 看到了新值

结论:每次读都看「最新已提交」——所以同一事务内两次读可能不同(不可重复读)。

这就是隔离级别的意义:默认 REPEATABLE-READ 帮你「读得稳定」,代价是背后有快照机制(MVCC)和多一点开销。多数业务用默认即可。

4. 四个隔离级别(一张表背下来)

隔离级别脏读不可重复读幻读说明
READ UNCOMMITTED可能可能可能能读到别人未提交的数据(几乎不用)
READ COMMITTED不会可能可能读已提交;Oracle 默认
REPEATABLE READ不会不会InnoDB 下基本不会MySQL 默认(快照读)
SERIALIZABLE不会不会不会串行化,最安全最慢

名词解释(面试三兄弟):

  • 脏读:读到别人还没提交、可能回滚的数据;
  • 不可重复读:同一事务里两次读同一行,值变了(因为别人提交了);
  • 幻读:同一事务里两次范围查询,行数变了(多出来/少了行)。InnoDB 在 REPEATABLE READ 用间隙锁 + MVCC 基本杜绝了幻读。

设置级别(会话级常用):

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 全局(谨慎)

5. 背后机制:MVCC + 锁(够用的直觉)

  • MVCC(多版本并发控制):每个事务读写时基于一个「快照版本」,读不阻塞写、写不阻塞读——这就是 RR 下 A 能「稳定读到旧值」而 B 能正常提交的原因;
  • 行锁:写同一行才互相等待(SELECT ... FOR UPDATE 会显式加行锁);
  • 间隙锁:范围条件锁住「区间」,防别的插入造成幻读;
  • 死锁:两个事务互相等对方持有的锁。MySQL 会自动检测并回滚一方(报 Deadlock found),你的代码要能重试被回滚的事务

直觉版:读走快照(MVCC),写走锁。SELECT 普通读不堵人;要「读了之后锁住防止别人改」才用 SELECT ... FOR UPDATE

6. 实战心法(给你写业务用)

  1. 事务要短:别在事务里做网络请求、慢查询——锁持有越久越容易死锁/阻塞;
  2. 改多行按固定顺序(比如都按 id 升序),可显著降低死锁概率;
  3. 扣库存/转账这类读改写,用 SELECT ... FOR UPDATE 或原子 SQL(UPDATE ... SET x = x - n WHERE x >= n)避免超卖;
  4. 一次事务里读一致性有要求 → 用默认 RR 就好;追求更低锁开销、能接受读到新值 → READ COMMITTED(很多互联网团队这么配);
  5. 连接池/框架事务注解别滥用:只有「真需要一起成功」的多步写才开事务。

动手(用 shop,开两个终端)

  1. 两个终端都连上,各 SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
  2. A:BEGIN; SELECT amount FROM orders WHERE id=1;
  3. B:UPDATE orders SET amount=amount+1 WHERE id=1; COMMIT;
  4. A 再查一次——值没变(RR 快照);
  5. 把两边隔离级别改成 READ COMMITTED 重试——A 第二次能看到新值。

自测

  1. 事务的 BEGIN / COMMIT / ROLLBACK 各干什么?
  2. MySQL 默认隔离级别是?它怎么保证「同一事务内两次读一致」?
  3. 脏读 / 不可重复读 / 幻读分别是什么?
  4. 为什么事务里别做慢请求?
  5. 出现死锁报错后,业务代码该做什么?

下一篇:表设计与范式——别让表设计成为项目的债。