☰
MySQL事务隔离级别详解:MVCC与锁如何解决并发异常
2026/10/3 14:30:14 网站建设 项目流程

做了这么多年MySQL,我越来越觉得事务隔离级别是一个特别值得花时间吃透的东西。你可能平时只是写完INSERT、UPDATE就结束了,根本没有考虑过隔离级别这回事,但线上一旦出现"事务A查到旧数据"、"事务B插入数据导致结果集突变"、"死锁日志显示gap lock冲突"这类问题,最后追根溯源都会落到隔离级别和底层锁机制上。更何况,MySQL的隔离级别几乎是Java后端面试绕不开的必考题,从"脏读、不可重复读、幻读的区别"到"为什么MySQL默认用可重复读",每一层都能看出候选人到底是背了概念还是真在项目里踩过坑。这篇文章我就把MySQL事务隔离级别的能力边界、MVCC与锁的底层实现、完整的查看设置方式、生产环境选型思路,以及面试和实战中容易被忽略的细节一次讲清楚。

1. 四个隔离级别的能力边界和三种并发异常

1.1 从读未提交到串行化:每个级别到底隔离了什么

SQL标准定义了四种隔离级别,隔离能力从低到高排:读未提交(READ UNCOMMITTED)、读已提交(READ COMMITTED)、可重复读(REPEATABLE READ)、串行化(SERIALIZABLE)。

先看每个级别最直观的行为差异。假设订单系统里有一个库存表,事务A执行了扣减库存的UPDATE但还没有提交,此时事务B去查询这条库存记录:

在读未提交级别下,B能直接读到A尚未提交的那个"扣减后"数值,跟业务上还没定论的数据发生交互,非常危险。这个级别平时基本没人会用,除了少数做统计、对一致性完全无要求的场景,我基本不推荐碰它。

在读已提交级别下,B只能读到已经提交的数据。如果A还没提交,B读到的是旧库存;如果A提交了,B再查就变成扣减后的新值。这个级别能有效避免脏读,也是Oracle、PostgreSQL等数据库的默认级别。

在可重复读级别下,B在事务内第一次执行查询时,相当于给自己拍了一张"事务启动时刻的快照",这个事务后续所有普通查询都基于这张快照,不会受到其他事务提交的影响。所以哪怕A提交了扣减,B在这个事务里反复查库存,看到的始终是快照里那个旧值。MySQL InnoDB的默认隔离级别就是可重复读。

在串行化级别下,事务之间的并发被压到最低,读写都要排队拿锁,隔离性最强但是吞吐量通常也是最低的。

这里有个很重要的认知:隔离级别决定的是"数据可见性边界",不是"是否加锁"。串行化也并不是完全没有锁,读未提交也一样有锁来保证写操作本身不冲突,只是它允许读到未提交数据。很多人把这个概念混在一起,后面看锁机制时会容易绕晕。

1.2 脏读、不可重复读、幻读是如何发生的

三个经典的并发异常,本质上都是事务之间互相看到了不该看到的数据状态。

脏读对应的是"未提交的数据"。事务A改了库存但没提交,事务B读到了这个修改值。过一会儿A回滚了,这行数据恢复原状,B之前读到的那个中间状态在业务上根本没发生过,所以叫"脏"数据。防止脏读的最低要求就是读已提交。

不可重复读对应的是"已提交的被修改数据"。同一个事务里,第一次SELECT查询某一行得到值X,另一个事务对这个行做了UPDATE并提交,当前事务第二次SELECT同一行得到了值Y。两次读取的结果对不上,这在账务核对、报表统计这类业务里是致命的。防止不可重复读,需要把级别提升到可重复读。

幻读对应的是"已提交的新插入数据"。事务内两次执行同一条范围查询,第一次查出5行,另一个事务插入了一条符合条件的新记录并提交,第二次查出6行。多出来的那行像幽灵一样凭空出现,所以叫幻读。不可重复读关注的是同一行的值变了,幻读关注的是结果集的行数多了,这是两者最核心的区别。

我用一句话总结这三者的关系:脏读是读到了还没定论的数据,不可重复读是同一行数据前后不一致,幻读是同一范围的行集合前后不一致。

1.3 一张表理清异常和隔离级别的对应关系

隔离级别脏读不可重复读幻读
读未提交可能发生可能发生可能发生
读已提交不会发生可能发生可能发生
可重复读不会发生不会发生InnoDB下基本不会发生
串行化不会发生不会发生不会发生

注意表格里可重复读那列,SQL标准里可重复读并不能避免幻读,但MySQL InnoDB通过MVCC配合间隙锁,把可重复读级别的幻读问题基本堵住了,这一点也是后面要说到的实现层面的重点。记住这个表格,面试时先答标准定义,再补充InnoDB的实现差异,会显得你既有理论又有实战认知。

2. MVCC和锁的配合:隔离级别在底层是怎么实现的

2.1 快照读和当前读是两套完全不同的路径

想理解InnoDB下隔离级别的行为,必须先分清两条读取路径:快照读(snapshot read)和当前读(current read)。

平时写的普通SELECT,不带FOR UPDATE、不带LOCK IN SHARE MODE、也不在UPDATE或DELETE里依赖读取,走的就是快照读。快照读不加锁,通过MVCC机制去读取undo log版本链上符合可见性规则的历史版本。因为不加锁,它的并发性能非常好,这也是MySQL事务处理吞吐量高的一个底层原因。

UPDATE、DELETE、INSERT,以及SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE,走的是当前读。当前读必须读取行记录的最新已提交版本,同时给命中的记录加锁,保证在操作期间没有其他事务干扰。

这两条路径的区别直接决定了隔离级别行为上的微妙差异。比如在可重复读级别下,一个事务里频繁执行普通SELECT,反复读到的都是同一个快照;但如果这个事务里执行了UPDATE,UPDATE走的是当前读,它会看到比快照更新的已提交数据。这就可能出现一种看似矛盾的现象:明明两次SELECT结果一致,一旦中间执行了UPDATE再回查,数据的可见范围就变了。这不是bug,是快照读和当前读各自遵循了不同的规则。

2.2 undo log版本链和Read View如何支撑可重复读

InnoDB中每行数据除了业务字段,还藏着几个关键的系统列:DB_TRX_ID(最近一次修改这行数据的事务ID)、DB_ROLL_PTR(回滚指针),以及隐藏主键等。一行数据每次被更新,修改前的旧版本都会被写进undo log,DB_ROLL_PTR则把同一行的多个版本串成一条链表,这就是版本链。

快照读时,InnoDB通过一个叫Read View(读视图)的机制来判断版本链上哪个版本对当前事务可见。Read View本质上是在生成那一刻对"活跃事务"的一张快照,里面记录了几个关键值:活跃事务ID列表、最小活跃事务ID、最大事务ID+1。判断规则大致是:

  • 版本的事务ID比最小活跃事务ID还小,说明这个版本在Read View生成前就已经提交,可见。
  • 版本的事务ID落在活跃事务列表里,说明生成快照时它还没提交,不可见。
  • 版本的事务ID大于等于最大事务ID+1,说明它是在快照生成之后才启动的事务,不可见。

如果当前版本不可见,就沿着版本链往上找,找到第一个可见版本为止。

可重复读和读已提交在Read View生成策略上的差别,正是两种隔离级别行为差异的根源。可重复读下,事务第一次执行快照读时生成Read View,之后整个事务期间所有的快照读都复用这一个Read View,所以无论别的会话提交多少次修改,当前事务看到的始终是"最初那一版世界"。读已提交下,每次执行快照读都会重新生成一个Read View,所以同一个事务内两次SELECT可能看到不同数据——第一次查完之后其他事务提交更新,第二次再查就能看到最新的已提交值,这就是不可重复读产生的底层过程。

2.3 间隙锁如何从根源上阻断幻读

说到幻读只有MVCC还不够。MVCC主要解决快照读场景下的可见性,但当前读场景下,事务执行SELECT ... FOR UPDATE或者UPDATE时,如果其他事务往范围里插入了新行,还是可能造成行数突变。InnoDB用间隙锁(gap lock)来处理这个问题。

间隙锁锁的不是某行记录,而是索引记录之间的区间。比如一段索引里分布着值10、20、30,间隙锁会锁住(10,20)、(20,30)这些开区间,其他事务无法在这个区间插入11、21这类新记录。在可重复读级别下,InnoDB在执行当前读时,除了给命中的记录加行锁,还会给扫描范围涉及的间隙加间隙锁,这样并发事务往同一个范围插数据就会被阻塞,幻读从锁的层面被堵住了。

这个设计也带来一些副作用,比如间隙锁之间互相兼容,但间隙锁与插入意向锁冲突,两个事务互相在对方的间隙里插入数据时容易触发死锁。生产环境里我用MySQL遇到过不少死锁日志,排查下来基本都指向"可重复读+当前读+范围扫描+并发插入"的组合。这也是为什么有些团队宁可把隔离级别改成读已提交、牺牲一点隔离性换取更低的死锁概率和更高的并发度。

3. 查看、设置和验证隔离级别的完整实操

3.1 查看当前隔离级别的几种方式

开发中随时需要确认当前会话或整个实例用的是什么隔离级别。MySQL里最常用的查看语句是:

SELECT @@transaction_isolation;

在MySQL 5.7及更早版本,这个变量名是tx_isolation,8.0开始变成了transaction_isolation。老版本升级之后如果还习惯写前者,会得到一个空值。

-- MySQL 8.0 / 5.7+ SELECT @@global.transaction_isolation; -- 全局级别 SELECT @@session.transaction_isolation; -- 会话级别 SELECT @@transaction_isolation; -- 当前生效值

也可以直接用SHOW VARIABLES:

SHOW VARIABLES LIKE 'transaction_isolation';

需要注意的是,会话级别的隔离级别只对当前连接生效,一旦断开重连就会恢复全局设置。如果你在排查某个连接的行为异常,先确认这个连接是不是被应用层设置过会话级隔离级别,这是一个很容易忽略的排查点。

3.2 修改隔离级别的三种方式

修改隔离级别有三个作用范围:全局、会话、下一次事务。

-- 全局生效,新连接生效,已有连接不受影响 SET GLOBAL transaction_isolation = 'READ-COMMITTED'; -- 当前会话生效,影响之后所有事务 SET SESSION transaction_isolation = 'REPEATABLE-READ'; -- 只对下一个事务生效 SET transaction_isolation = 'SERIALIZABLE';

全局修改不用重启数据库,但对已经存在的连接不生效,实际业务里如果要做全局切换,要注意连接池里的连接未必会立马切到新配置,很多老连接可能还带着旧隔离级别跑很久,这是生产变更里一个非常隐蔽的坑。

除了SET语句,MySQL还支持配置文件方式设置:

[mysqld] transaction-isolation = READ-COMMITTED

设置完成后建议用status或information_schema里的表确认变更是否真正生效,特别是线上环境,变更前后都查一遍,避免出现"改了但没起作用"的假象。

3.3 用SQL复现一次不可重复读

理论知识讲再多,不如亲手跑一遍理解深刻。这里用一个最经典的两连接实验来复现不可重复读。

准备一张表:

CREATE TABLE account ( id INT PRIMARY KEY, balance DECIMAL(10,2) ) ENGINE=InnoDB; INSERT INTO account VALUES (1, 100.00);

会话A先设置成读已提交:

-- 会话A SET SESSION transaction_isolation = 'READ-COMMITTED'; START TRANSACTION; SELECT balance FROM account WHERE id = 1; -- 结果 100.00

这时候别提交,打开会话B执行:

-- 会话B UPDATE account SET balance = 200.00 WHERE id = 1; COMMIT;

再回会话A执行第二次查询:

SELECT balance FROM account WHERE id = 1; -- 结果 200.00

同一个事务里两次查询,结果从100.00变成了200.00,不可重复读完美复现。把会话A的隔离级别改成REPEATABLE-READ再跑一遍,第二次查询结果仍然是100.00。如果你想复现幻读,可以把实验从单行查询改成范围查询,在会话B里插入一条id=2的记录并提交,然后在可重复读级别下看结果集行数是否会变化——普通快照读下不会变,但加上FOR UPDATE的当前读会被间隙锁挡住,这些细节跑一遍就全明白了。

4. 生产环境选型:为什么MySQL默认可重复读,实际又该怎么选

4.1 默认可重复读是历史包袱还是刻意设计

MySQL InnoDB的默认隔离级别是可重复读,而Oracle、PostgreSQL默认是读已提交,这一点经常被拿出来讨论。MyISAM时代没有事务,转而InnoDB引入事务时为了兼容某些历史行为,默认选择了可重复读。但发展到今天,我倾向于认为这是InnoDB在MVCC和间隙锁加持下的一种刻意默认:它让想要更高隔离性的业务在默认配置下就能获得较好的保护,同时MVCC又让它保持了足够好的并发性能,不会因为默认级别高一点就让吞吐量崩盘。

需要特别说明的是,SQL标准里可重复读是不能完全防幻读的,但InnoDB在可重复读下通过MVCC解决了快照读的幻读问题,通过间隙锁解决了当前读的幻读问题。所以在MySQL语境里,"可重复读能防幻读"这个说法在绝大多数场景下是成立的。这也是面试官最爱追问的点,标准定义和MySQL实现之间的差异一定要能讲清楚。

4.2 隔离级别、锁范围与并发性能的权衡

隔离级别越高,事务之间越互不干扰,但代价是锁的范围更大、持有时间更长、并发度更差。可重复读要用间隙锁防幻读,间隙锁本身就是并发插入的绊脚石;改成读已提交后,InnoDB只保留行锁,不加间隙锁,并发插入能力显著提升,死锁概率下降。

真实业务里,很多互联网团队会把默认隔离级别改成读已提交,因为大部分业务并不依赖"事务内多次读取同一快照"的一致性,而更在意高并发写入时的吞吐量。比如订单、库存、日志类系统,宁可接受读已提交下"同一事务两次范围查询可能看到不同数据",也不愿意高频死锁拖垮线上。这里没有绝对的对错,只有取舍。

我的建议是:如果业务里存在"事务开始后,多次查询必须基于同一份快照做一致性判断"的强需求,比如账务核对、对账单生成、复杂统计报表,保持可重复读;如果业务以高并发写入为主,范围查询并发插入频繁、死锁率高,可以考虑切到读已提交,但要在应用层设计好补偿逻辑。

4.3 从隔离级别看到分布式事务的边界

隔离级别解决的是单个数据库实例内部事务并发的问题,但如今订单系统和库存系统经常拆成两个独立服务,各自有独立的数据库,这时候就需要分布式事务。很多人把MySQL事务隔离级别和分布式事务混在一起谈,实际上它们的层次完全不同。

隔离级别管的是"一个库内多个事务之间的数据可见性",分布式事务管的是"多个资源管理器(多个库、或者库加消息队列)之间的数据一致性"。分布式事务最终要保证的通常是全局的原子提交,比如订单系统扣减了库存,消息队列也记录了事件,要么全部成功,要么全部回滚。这时候即使每个库都设置成串行化,也不能解决跨库一致性问题,因为协调机制在数据库之外。

可以换一个角度理解:隔离级别决定了一件事务内部能看到什么,分布式事务决定了多个事务在跨系统边界时如何协调。理解了隔离级别的边界,你就知道"线下数据库调成可重复读就能解决分布式问题"是不成立的,分布式场景需要分布式事务协调方案来完成跨库的最终一致或强一致。

5. 面试和实战中高频出现的问题与边界细节

5.1 可重复读级别下为什么还会有"新鲜"数据

这是很多开发在实际项目中产生困惑的经典问题。可重复读明明说事务内多次读取结果一致,为什么有时候执行了UPDATE之后,再SELECT能看到其他事务提交的新数据?

答案是快照读和当前读的差异。可重复读的一致性读(快照读)依赖事务启动后的第一个Read View,但UPDATE走的是当前读,它必须读取最新已提交版本。当前读会把新值写回来,之后事务内的快照读再读取这行,就可能会跟随这个最新版本,而不是最初的那份快照。这一点在一些事务里执行"先查后改再查"的业务逻辑时特别容易踩坑,搞清楚快照读和当前读的区别,这类问题就迎刃而解了。

5.2 谈隔离级别必谈锁:记录锁、间隙锁和意向锁

四个隔离级别不是四条独立的路,它们是在"是否加锁、加什么锁、何时释放锁"这几张表上做选择。记录锁锁的是索引记录本身;间隙锁锁的是记录之间的空隙,防插入;临键锁(next-key lock)是记录锁加间隙锁的组合,InnoDB在可重复读级别下做范围扫描时默认用的就是临键锁,这也是防幻读的关键。

此外还有表级意向锁,比如事务准备给某行加锁时,先要在表上加意向锁,与表锁做冲突判断,避免锁升级导致全局等待。面试时如果能在谈隔离级别时把记录锁、间隙锁、临键锁、意向锁各自的作用串起来讲,会明显体现出你对InnoDB并发控制的完整理解。

5.3 隔离级别和事务日志的关系

线上经常碰到一类报错,提示某个数据库的事务日志已满。这类问题和隔离级别虽然不直接相关,但都落在同一个大的事务体系里。事务执行过程中产生的undo log和redo log都是持久化到磁盘文件的,如果事务长时间不提交、操作量巨大,或者并发事务过多,日志文件可能被写满。

我遇到过一次典型的案例:一个批量更新任务在一个事务里UPDATE了几十万行,长时间不提交,把所有空闲日志空间耗尽,导致其他正常事务也被阻塞。排查下来发现,真正的问题不是隔离级别配置错了,而是大事务拆分不充分。这类经验告诉我们,理解事务隔离级别不能只盯着SELECT和UPDATE的可见性,还要把事务长度、日志空间、锁持有时间放在一起考量。重构为分批提交的小事务,往往比调隔离级别更能解决线上的性能和阻塞问题。

5.4 我的一点实际操作心得

最后分享一个我自己的习惯。上线前,我会把每条关键SQL在目标隔离级别下做一次并发场景验证,不只看返回值对不对,还要看锁等待、死锁日志和事务隔离级别相关的状态变量变化。比如用SHOW ENGINE INNODB STATUS查看最近的事务和锁信息,用performance_schema里的表监控等待事件。这些东西平时不起眼,一旦线上出了并发问题,它们就是定位的第一手素材。

以后再遇到"同一个事务查两次结果不一样"的问题,先别急着怀疑数据库出bug,第一反应去查当前连接的隔离级别,第二反应复盘事务里是快照读还是当前读,第三反应看有没有大事务挤占日志空间。把这三个点排查下来,九成以上的事务一致性问题都能找到根因。当然,手工去执行这些验证步骤确实需要一点耐心,但一旦你亲手复现过脏读、不可重复读或者幻读,再看官方文档里那些抽象描述,就会发现原来它们说的每一句话,都是你在实验里看到过的真实现象。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询