1. 从一道面试题说起:为什么隔离级别总是"背了又忘"
先讲个真实的场景。前阵子帮团队做技术面试,问到"MySQL默认隔离级别是什么"时,十个人里有八个能答出"可重复读",但追问到"为什么 InnoDB 要把默认级别设成可重复读,而不是业界更常见的读已提交"时,基本都会卡住。再往下问"可重复读到底解决了什么问题,又留下了什么坑",能说清楚的人就更少了。
这个现象很典型。隔离级别在 MySQL 的知识体系里属于那种"看着简单、真正用起来全是细节"的内容。它不像索引优化那样有明确的性能反馈,也不像主从复制那样有清晰的架构图。它是一套关于"并发事务之间如何互相隔离"的规则,出问题的时候表现为业务数据错乱、重复插入、统计结果对不上——这些问题往往不是立刻爆发的,而是在某个并发量上来之后突然出现,排查起来非常痛苦。
这篇文章想做的事情,是把 MySQL 事务隔离级别这条线完整捋一遍。从底层原理到实操验证,从四种隔离级别的表现差异到 InnoDB 具体怎么实现,再到面试里经常被追问的边角问题。内容会尽量贴近实战,很多案例是我自己在测试环境里跑过的,也会带上一些踩坑记录。
先给一个整体的认知框架:隔离级别本质上是在并发能力和数据一致性之间做权衡。隔离得越彻底,并发能力越差;隔离得越松,数据越容易出问题。MySQL 提供了四种级别——读未提交、读已提交、可重复读、串行化,从松到严排列。但真正理解它们,光记住名字没用,你得知道每个级别下到底会发生什么、为什么发生、怎么避免。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 四种隔离级别下到底会发生什么:用一个银行转账场景逐一验证
先建立一个最小可复现的实验环境。这个环境我建议你也在本地搭一下,后面所有讨论都基于实际验证,不是纸上谈兵。
sql复制-- 创建测试库和表
CREATE DATABASE IF NOT EXISTS test_isolation;
USE test_isolation;
CREATE TABLE account (
id INT PRIMARY KEY AUTO_INCREMENT,
user_name VARCHAR(50) NOT NULL,
balance DECIMAL(10,2) NOT NULL DEFAULT 0
) ENGINE=InnoDB;
INSERT INTO account (user_name, balance) VALUES ('Alice', 1000.00), ('Bob', 1000.00);
表格结构很简单,一个用户余额表。开两个终端连接 MySQL,模拟两个并发事务,后续所有实验都在这个基础上进行。
2.1 读未提交:脏读的温床,现实中几乎没人用
读未提交(READ UNCOMMITTED)的含义是:一个事务可以读到另一个事务尚未提交的数据。这是最宽松的隔离级别,也是并发能力最强的级别,代价是数据一致性几乎无法保证。
实验步骤:在终端 A 开启事务并更新 Alice 的余额,但不提交;在终端 B 查询 Alice 的余额。
sql复制-- 终端A
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
UPDATE account SET balance = 900.00 WHERE user_name = 'Alice';
-- 终端B
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT balance FROM account WHERE user_name = 'Alice';
终端 B 查到的结果是 900.00。这个数据是终端 A 改完但还没提交的。如果终端 A 这时候执行 ROLLBACK,那终端 B 读到的 900.00 就成了一个从来不曾存在过的数据——这就是脏读。
实际业务里几乎没有人会用读未提交,除非是一些对数据准确性完全不敏感且追求极致性能的场景。MySQL 支持这个级别主要是为了 SQL 标准兼容,你在生产环境的大多数配置里基本见不到它。面试时如果有人问你哪个级别最危险,答案就是这个。
2.2 读已提交:解决了脏读,但引入了不可重复读
读已提交(READ COMMITTED)的含义是:一个事务只能读到其他事务已提交的数据。这是很多数据库(如 Oracle、PostgreSQL)的默认隔离级别,但在 MySQL 里它不是默认值。
继续用上面的表验证。把两个终端都切到读已提交:
sql复制-- 终端A
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT balance FROM account WHERE user_name = 'Alice'; -- 查到 1000.00
-- 先不提交,等终端B操作
-- 终端B
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
UPDATE account SET balance = 800.00 WHERE user_name = 'Alice';
COMMIT;
-- 终端A再次查询
SELECT balance FROM account WHERE user_name = 'Alice'; -- 这次查到 800.00
这里有个关键现象:终端 A 在同一个事务里执行了两次相同的 SELECT,但查到的结果不一样。第一次是 1000.00,第二次变成 800.00。这就是不可重复读——在同一个事务内,两次读取同一行数据,结果被其他事务的提交影响了。
读已提交确实防住了脏读(读不到未提交的数据),但防不住不可重复读。它的实现机制是:每次 SELECT 都会生成一个新的快照(或者说,读取的是当前时刻已经提交的最新版本)。所以同一个事务里不同时刻的读操作,看到的是不同时间点的数据状态。
2.3 可重复读:MySQL 的默认级别,也是面试最爱深挖的级别
可重复读(REPEATABLE READ)的含义是:一个事务开始后,多次读取同一行数据,结果始终一致,不受其他事务提交的影响。这是 MySQL InnoDB 的默认隔离级别。
同样的实验切换过来:
sql复制-- 终端A
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT balance FROM account WHERE user_name = 'Alice'; -- 查到 1000.00
-- 终端B
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
