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