MySQL事务隔离级别详解
MySQL 事务隔离级别详解
记得刚入职的时候,线上出了个诡异的 bug:用户 A 给自己账户充值了 100 块,结果余额没变。查了半天,发现是事务隔离级别没搞懂,导致一个事务读到了另一个事务未提交的数据。
今天咱们就来彻底搞懂 MySQL 的事务隔离级别,看完这篇,你就能避开 90% 的事务坑。
事务的 ACID 是啥?
先铺垫一下,事务有四大特性,记住这个面试必问:
- A(Atomicity)原子性:事务要么全做,要么全不做,不可能只执行一半
- C(Consistency)一致性:事务执行前后,数据库都得是"合法"的状态
- I(Isolation)隔离性:多个事务并发执行,彼此之间不能互相干扰
- D(Durability)持久性:事务提交后,数据就得永久保存,哪怕数据库宕机
今天重点是I(隔离性),也就是隔离级别。
没有隔离级别会出啥问题?
如果多个事务完全不隔离,会有三种经典问题:
1. 脏读(Dirty Read)
事务 A 读到了事务 B未提交的数据。如果事务 B 后面回滚了,那事务 A 读到的就是"脏数据"。
时间轴: T1: 事务A开始 T2: 事务B开始 T3: 事务B把小明余额从100改成200(未提交) T4: 事务A读取小明余额,读到200 ← 脏读! T5: 事务B回滚,小明余额变回100 T6: 事务A拿着200去干活,实际上余额只有1002. 不可重复读(Non-Repeatable Read)
同一个事务内,两次读取同一行数据,结果不一样(因为别的事务修改并提交了这行数据)。
时间轴: T1: 事务A开始,读取小明余额=100 T2: 事务B开始,把小明余额改成200,提交 T3: 事务A再次读取小明余额=200 ← 不可重复读!3. 幻读(Phantom Read)
同一个事务内,两次执行同样的查询,记录数量不一样(因为别的事务插入或删除了数据)。
时间轴: T1: 事务A开始,查询余额>100的用户,得到10条 T2: 事务B开始,插入一个余额150的新用户,提交 T3: 事务A再次查询余额>100的用户,得到11条 ← 幻读!注意:不可重复读是针对已有记录的修改,幻读是针对记录数量的变化。
MySQL 的四种隔离级别
MySQL 定义了四种隔离级别,从低到高:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED(读未提交) | ❌ 会 | ❌ 会 | ❌ 会 |
| READ COMMITTED(读已提交) | ✅ 不会 | ❌ 会 | ❌ 会 |
| REPEATABLE READ(可重复读) | ✅ 不会 | ✅ 不会 | ❌ 会(InnoDB 不会) |
| SERIALIZABLE(串行化) | ✅ 不会 | ✅ 不会 | ✅ 不会 |
MySQL 默认隔离级别是 REPEATABLE READ(可重复读),而且 InnoDB 通过 Next-Key Lock 解决了幻读问题,所以实际上 REPEATABLE READ 已经能防住所有问题了。
实战:看看每个隔离级别的表现
咱们用实际 SQL 来验证一下,假设你有个账户表:
CREATETABLEaccounts(idINTPRIMARYKEY,nameVARCHAR(50),balanceDECIMAL(10,2));INSERTINTOaccountsVALUES(1,'小明',100.00);1. READ UNCOMMITTED(读未提交)
-- 会话ASETSESSIONTRANSACTIONISOLATIONLEVELREADUNCOMMITTED;STARTTRANSACTION;SELECTbalanceFROMaccountsWHEREid=1;-- 读到 100-- 会话B(另一个终端)STARTTRANSACTION;UPDATEaccountsSETbalance=200WHEREid=1;-- 注意:这里没提交!-- 会话ASELECTbalanceFROMaccountsWHEREid=1;-- 读到 200!脏读!问题:会话 A 读到了会话 B 未提交的数据。如果会话 B 回滚,那会话 A 拿到的 200 就是无效的。
2. READ COMMITTED(读已提交)
-- 会话ASETSESSIONTRANSACTIONISOLATIONLEVELREADCOMMITTED;STARTTRANSACTION;SELECTbalanceFROMaccountsWHEREid=1;-- 读到 100-- 会话BSTARTTRANSACTION;UPDATEaccountsSETbalance=200WHEREid=1;-- 没提交-- 会话ASELECTbalanceFROMaccountsWHEREid=1;-- 还是读到 100(防脏读)-- 会话BCOMMIT;-- 会话ASELECTbalanceFROMaccountsWHEREid=1;-- 读到 200!不可重复读!问题:同一个事务内,两次读取结果不一样(不可重复读)。
Oracle 默认就是这个级别,但 MySQL 默认不是。
3. REPEATABLE READ(可重复读,MySQL 默认)
-- 会话ASETSESSIONTRANSACTIONISOLATIONLEVELREPEATABLEREAD;STARTTRANSACTION;SELECTbalanceFROMaccountsWHEREid=1;-- 读到 100-- 会话BSTARTTRANSACTION;UPDATEaccountsSETbalance=200WHEREid=1;COMMIT;-- 会话ASELECTbalanceFROMaccountsWHEREid=1;-- 还是读到 100!可重复读!牛逼之处:同一个事务内,多次读取同一行,结果一致。MySQL 是怎么做到的?MVCC(多版本并发控制)!
简单说就是:每个事务开始时,会生成一个快照(Read View),后面的查询都基于这个快照,不受其他事务提交的影响。
那如果会话 A 想更新呢?
-- 会话AUPDATEaccountsSETbalance=balance+50WHEREid=1;-- 这里会加行锁,并且读取最新的值(200),所以结果是 250SELECTbalanceFROMaccountsWHEREid=1;-- 读到 250注意:UPDATE/DELETE/INSERT 操作会读取最新数据(当前读),不受快照影响。
4. SERIALIZABLE(串行化)
-- 会话ASETSESSIONTRANSACTIONISOLATIONLEVELSERIALIZABLE;STARTTRANSACTION;SELECT*FROMaccountsWHEREid=1;-- 会话BSTARTTRANSACTION;INSERTINTOaccountsVALUES(2,'小红',300);-- 阻塞!等会话A提交才能执行原理:SERIALIZABLE 会对所有读取加共享锁,写操作加排他锁,相当于事务排队执行,性能最差,但最安全。
一般不用,除非对数据一致性要求极高(比如金融场景)。
InnoDB 怎么解决幻读的?
前面我说了,InnoDB 的 REPEATABLE READ 能防幻读,这是怎么做到的?
答案是:Next-Key Lock(临键锁)。
假设你的账户表有这些记录:
INSERTINTOaccountsVALUES(1,'小明',100),(5,'小红',200),(10,'小刚',300);如果会话 A 执行:
-- 会话ASTARTTRANSACTION;SELECT*FROMaccountsWHEREid>5FORUPDATE;-- 命中了 id=10 这条记录InnoDB 不会只锁id=10这一行,而是锁住(5, 10]这个区间(Next-Key Lock = 记录锁 + 间隙锁)。
那会话 B 想插入id=7的记录:
-- 会话BINSERTINTOaccountsVALUES(7,'小华',150);-- 阻塞!因为 (5, 10] 被锁住了结果:会话 B 必须等会话 A 提交后才能插入,幻读就不可能发生了。
如何查看和设置隔离级别?
查看当前隔离级别
-- MySQL 8.0+SELECT@@transaction_isolation;-- 或者SHOWVARIABLESLIKE'transaction_isolation';-- MySQL 5.7 及之前SELECT@@tx_isolation;设置隔离级别
-- 设置当前会话的隔离级别(只对当前连接有效)SETSESSIONTRANSACTIONISOLATIONLEVELREADCOMMITTED;-- 设置全局隔离级别(对新连接有效,当前连接不变)SETGLOBALTRANSACTIONISOLATIONLEVELREADCOMMITTED;-- 修改配置文件(永久生效)-- 在 my.cnf 或 my.ini 里加:[mysqld]transaction-isolation=READ-COMMITTED实战建议
1. 别随便改隔离级别
MySQL 默认 REPEATABLE READ 是经过大量实践验证的,能解决绝大多数问题。除非你有明确理由,否则别改。
我们公司就有人把隔离级别改成 READ COMMITTED,结果出了不可重复读的 bug,排查了一整天。
2. 长事务是性能杀手
隔离级别越高,事务之间的隔离性越好,但并发性能越差。如果你开了个长事务(比如跑了 10 分钟还没提交),会阻塞很多其他操作。
-- 查看当前运行的事务SELECT*FROMinformation_schema.innodb_trx;建议:事务要尽量短小,用完马上提交。
3. 慎用 SELECT … FOR UPDATE
SELECT ... FOR UPDATE会加排他锁,容易引发死锁。
-- 会话ASTARTTRANSACTION;SELECT*FROMaccountsWHEREid=1FORUPDATE;-- 会话BSTARTTRANSACTION;SELECT*FROMaccountsWHEREid=2FORUPDATE;UPDATEaccountsSETbalance=100WHEREid=1;-- 阻塞-- 会话AUPDATEaccountsSETbalance=200WHEREid=2;-- 死锁!建议:只在必要时用FOR UPDATE,并且尽量按固定顺序加锁(比如按 id 排序)。
总结
- 事务隔离级别从低到高:READ UNCOMMITTED → READ COMMITTED → REPEATABLE READ → SERIALIZABLE
- 隔离级别越低,并发性能越好,但数据一致性越差
- MySQL 默认 REPEATABLE READ,通过 MVCC 防不可重复读,通过 Next-Key Lock 防幻读
- 别随便改隔离级别,慎用长事务和
SELECT ... FOR UPDATE
如果你能把 MVCC 和 Next-Key Lock 讲清楚,面试官绝对眼前一亮。
实战代码都在我本地跑过,你可以放心复制。如果有问题,欢迎评论区交流!
