MySQL数据库 (十四) MySQL事务(上),事务的四大特性,事务的操作方式,事务的隔离级别

📅 2026/7/14 6:51:25 👁️ 阅读次数 📝 编程学习
MySQL数据库 (十四) MySQL事务(上),事务的四大特性,事务的操作方式,事务的隔离级别

目录

一、引入事务

二、事务

什么是事务?

事务的属性

事务的版本支持

三、事务的提交

事务的提交方式

四、事务常见操作方式

提前准备:

事务的正常操作

事务异常验证

证明单条 SQL 与事务的关系

事务操作的注意事项

五、事务隔离级别

什么是事务的隔离性?

隔离级别

查看与设置隔离性

六、事务隔离级别

事务隔离级别 - 读未提交

事务隔离级别 - 读提交

事务隔离级别 - 可重复读

事务隔离级别 - 串行化

总结:

七、总结


本篇文章我们来开启一个全新的话题:MySQL 事务

在这之前,我们聊的都是索引,它解决的是 “怎么查得快” 的问题;而事务要解决的,是 “数据怎么保证安全可靠” 的问题。

一、引入事务

我们先来看图里这个经典的火车票售票场景,就能明白事务存在的必要性。

假设系统里只剩 1 张票了,数据表里的 nums 字段值为 1。这时候,客户端 A 和客户端 B 同时发起了买票请求:客户端 A 先查询,发现票数 nums > 0,于是准备卖票。但它还没来得及执行 update nums = nums - 1 更新数据库。几乎在同一时间,客户端 B 也查询了这张票,发现 nums 同样大于 0,于是也准备卖票。客户端 A 执行更新,把票数减为 0。客户端 B 再执行更新,也把票数减为 0。这样一来,一张票就被卖给了两个人,数据出现了严重的错误。这就是没有事务控制时,并发操作可能导致的问题。

要解决这个问题,我们就需要引入事务的概念,让数据操作满足四个关键特性,也就是我们常说的 ACID:

  1. 原子性(Atomicity)买票的整个过程必须是一个不可分割的整体,要么全部成功,要么全部失败。不能出现 “票卖出去了,但库存没更新” 或者 “库存扣了,但订单没生成” 的中间状态。
  2. 一致性(Consistency)事务执行前后,数据必须始终处于合法的状态。比如上面的例子,最终票数要么是 1(都没买到),要么是 0(被其中一个人买到),不能出现负数,也不能一张票卖给两个人。
  3. 隔离性(Isolation)多个客户端同时操作时,它们之间不能互相干扰。A 的事务在提交之前,B 不应该看到 A 还没更新的中间状态数据,这样就不会出现 “看到票还在,但实际已经被卖了” 的情况。
  4. 持久性(Durability):事务一旦提交成功,数据的修改就必须永久生效,即使服务器崩溃,数据也不能丢失。

事务的核心作用,就是通过这四个特性,保证数据在并发操作下依然安全、正确,这也是银行转账、电商下单这类关键业务的基础保障。

二、事务

什么是事务?

首先,我们来明确什么是事务事务本质上就是一组 SQL 语句的集合,这些 SQL 在逻辑上是一个不可分割的整体,共同完成一个业务任务。

举个最常见的例子:转账操作。我给朋友转 100 块钱,这个过程包含两条关键 SQL:一条是从我的账户扣掉 100 元,另一条是给朋友的账户加上 100 元。这两条 SQL 必须全部成功,或者全部失败,绝对不能出现 “我扣了钱,但朋友没收到” 的情况,这就是事务的核心意义 ——把多条 SQL 打包成一个整体来执行。事务主要用于处理操作量大、复杂度高的数据场景,比如删除一个用户的所有信息,可能需要同时删除用户基本信息、成绩、论坛记录等多条数据,这些操作合起来就构成了一个事务。

而且 MySQL 同一时刻会处理大量并发的事务请求,如果没有事务机制,多个事务同时操作同一份数据时,很容易出现数据不一致的问题;如果事务执行到一半出错,已经执行的部分也会导致数据混乱,而事务的机制就是为了解决这些问题。

事务的属性

接下来,我们来看事务的四个核心属性,也就是常说的 ACID 特性,这是事务的根本保障。

首先是原子性(Atomicity)。原子性的意思是,事务中的所有操作,要么全部完成,要么全部不完成,不存在中间状态。就像刚才的转账例子,扣钱和加钱这两个操作,必须同时成功,或者同时失败。如果扣钱成功了,但加钱失败了,事务就会触发回滚操作,把扣掉的钱再恢复回来,就好像这个事务从来没有执行过一样,不会留下任何中间状态的数据。

第二个是一致性(Consistency)。一致性指的是,事务执行前后,数据库的完整性约束没有被破坏,数据始终处于合法的状态。比如转账前后,你和朋友的账户总金额必须保持不变,不能凭空多出或者消失 100 元;再比如库存数据,不能出现负数。一致性是事务的最终目标,原子性、隔离性和持久性都是为了保证一致性而存在的。

第三个是隔离性(Isolation)隔离性是指,多个并发事务同时读写同一份数据时,它们之间不会互相干扰,避免因为交叉执行导致数据不一致。比如两个用户同时买同一张票,事务的隔离性会保证两个事务不会同时读取到 “有票” 的状态,从而避免超卖问题。MySQL 提供了不同的隔离级别,从低到高分别是读未提交、读提交、可重复读和串行化,不同的隔离级别对应了不同的并发控制能力和性能开销。

第四个是持久性(Durability)。持久性的意思是,事务一旦提交成功,对数据的修改就是永久的,即使数据库服务器崩溃或者重启,修改后的数据也不会丢失。MySQL 会通过日志等机制,把事务的修改记录持久化到磁盘上,保证数据的可靠性。

简单来说,我们可以把事务理解成一批 SQL 的打包操作,把这些 SQL 打包成一个事务对象,交给 MySQL 执行,由 MySQL 来保证这组 SQL 的 ACID 特性,让数据操作既安全又可靠。

为什么会出现事务?

事务被 MySQL 的开发者设计出来,本质上是为了给应用层提供更简单、更安全的编程模型。当我们用事务包裹一组 SQL 时,我们只需要考虑两种结果:要么提交,要么回滚。这样一来,就不用自己去处理网络异常、服务器宕机、并发修改数据等复杂的错误场景了。

事务帮我们屏蔽了底层的各种潜在问题,让我们可以专注于业务逻辑本身,而不用被各种异常情况的处理代码困扰。可以说,事务是为应用层服务的,它并不是数据库系统天生自带的特性。

事务的版本支持

并不是所有 MySQL 存储引擎都支持事务,这是一个非常关键的知识点。在 MySQL 中,只有使用了 InnoDB 引擎的数据库或表,才支持事务;而我们之前提到的 MyISAM 引擎是不支持事务的。

我们可以通过 show engines \G 命令来查看 MySQL 支持的引擎及其事务能力:

  • InnoDB 引擎的 Transactions 字段为 YES,说明它完全支持事务,同时还支持行级锁和外键,这也是它成为 MySQL 默认引擎的重要原因。
  • MyISAM 引擎的 Transactions 字段为 NO,说明它不支持事务,无法保证数据的原子性和一致性,在并发场景下也无法处理数据安全问题。

所以,如果你需要使用事务,必须确保你的表使用的是 InnoDB 引擎,否则事务相关的操作(如commit、rollback)是不会生效的。

三、事务的提交

事务的提交方式

事务的提交方式,本质上决定了 MySQL 什么时候把我们对数据的修改永久保存下来,它主要分为两种:自动提交和手动提交,我们一个一个来说。

首先,我们来看怎么查看和修改当前的提交模式。MySQL 通过一个叫autocommit的系统变量来控制提交方式,我们可以用show variables like 'autocommit';来查看它的值。如果结果显示 autocommit | ON,说明当前处于自动提交模式;如果是 OFF,则是手动提交模式。我们也可以用 set autocommit=0; 来关闭自动提交,切换到手动模式;用 set autocommit=1; 来重新开启自动提交,切换到默认模式。

接下来我们说自动提交模式,也就是 autocommit=ON 的情况,这是 MySQL 的默认配置。在这种模式下,我们执行的每一条 SQL 语句,都会被 MySQL 当成一个独立的事务来处理。比如执行一条 update 修改数据,MySQL 会立刻自动提交这个事务,修改会直接永久写入数据库,没有机会再用 rollback 把它撤销回来。这种模式适合执行单条的、不需要回滚的简单操作,比如查询、一次性的修改语句,因为它不需要你额外做任何提交操作,用起来很方便。

然后是手动提交模式,也就是 autocommit=OFF 的情况。在这种模式下,MySQL 不会自动提交我们的修改,所有的 insert、update、delete 操作,都只是在当前会话的临时状态里生效,不会直接写入数据库。这时候,我们需要明确执行 commit 命令,MySQL 才会把所有修改永久保存;如果执行 rollback 命令,所有修改就会被撤销,数据会回到事务开始前的状态。这种模式是我们使用事务进行批量操作的基础,比如转账、订单创建这类需要多条 SQL 配合的业务,我们可以先执行所有操作,确认没问题再提交;如果中间出错,就直接回滚,避免数据被破坏。

需要注意的是,set autocommit 的设置只对当前会话有效,新的数据库连接会恢复到默认的自动提交模式,所以在实际开发中,我们通常会用 start transaction 来显式开启事务,这样就不用修改全局的 autocommit 配置了。

查看事务提交方式:

SET来改变MySQL的自动提交模式:

四、事务常见操作方式

我们继续来看事务的常见操作方式:

提前准备:

为了方便后续演示并发场景下的事务问题,我们首先把 MySQL 的默认隔离级别设置为 “读未提交”。执行 set global transaction isolation level read uncommitted; 语句,就能完成设置。设置完成后,可以用 select @@tx_isolation; 来查看当前的隔离级别,结果会显示为 READ-UNCOMMITTED,这一步是为了后续演示并发事务的现象做准备。

我们先创建测试表: 一个简单的员工工资表

接下来,我们创建一个用于演示的测试表 account,这是一个简单的员工工资表,包含 id、name和blance 三个字段,存储引擎指定为 InnoDB,字符集为 UTF8。

执行 create table 语句后,用 desc account; 查看表结构,可以看到三个字段的类型、约束都已按预期设置完成,blance字段默认值为0.00,方便我们后续进行余额修改的演示。

事务的正常操作

下面我们就开始演示,我们先来证明事务的开始与回滚:

接下来我们进入事务的核心操作演示,首先确认当前会话的自动提交状态。执行 show variables like 'autocommit';,结果显示 autocommit为ON,也就是 MySQL 默认的自动提交模式。我们故意保持这个默认设置,是为了验证 begin 或 start transaction 开启事务时,是否会不受自动提交模式的影响。

然后我们启动事务。开启事务有两种等价的方式:start transaction; 和begin;,两种方式执行后,MySQL 都会开启一个新的事务,后续的所有 SQL 语句都将属于这个事务,直到我们执行 commit 或 rollback 结束事务。这里推荐使用 begin,写法更简洁。开启事务后,无论当前 autocommit 是开启还是关闭,MySQL 都会进入手动控制模式,所有修改都不会自动提交,直到我们主动执行提交或回滚操作。
我们接着来看事务中保存点的用法,它能让事务的回滚操作变得更灵活,而不是只能撤销整个事务。

在开启事务之后,我们使用 savepoint 命令创建多个保存点,把事务的执行过程分成多个阶段。如上图,我们首先创建了保存点 s1,然后插入了第一条员工数据;接着创建保存点 s2,插入第二条数据;再创建保存点 s3,插入第三条数据。这三条插入操作都属于同一个事务,MySQL 会按照事务的 ACID 特性来管理它们,此时我们查询 account 表,能看到三条数据都已经存在,但这些修改还只是在当前事务的临时状态中,并没有被永久写入数据库。

现在我们进行回滚:

接下来我们演示部分回滚操作。首先执行 rollback to s3,这条命令会把事务回滚到 s3 保存点之前的状态,也就是撤销第三条插入操作。此时再次查询 account 表,只能看到前两条数据,第三条数据已经被撤销了。

接着我们执行 rollback to s1,这条命令会把事务回滚到 s1 保存点之前的状态,也就是撤销所有的插入操作,此时查询 account 表,结果会显示为空,所有数据都被回滚了。

最后,我们可以用 commit 命令提交事务,一旦提交,事务中所有未被回滚的修改都会被永久保存到数据库中,事务也随之结束。如果此时我们没有任何修改,提交后表中依然是空的;如果有部分修改没有被回滚,这些修改就会被永久生效。保存点的作用,就是让我们在事务中设置多个节点,遇到问题时只撤销部分操作,而不用从头再来,让事务的控制变得更灵活。


我们必须要设置保存点吗?

答案是否定的,保存点只是一个可选的增强功能,不是事务必须的配置。我们可以直接开启事务,执行多条 SQL 后,通过 rollback 直接回滚整个事务,而不需要设置任何保存点。

如上图,我们用 start transaction 开启事务,连续插入三条数据,然后直接执行 rollback,此时整个事务的所有修改都会被撤销,再次查询 account 表,结果会显示为空,说明所有插入操作都被成功回滚了。这种直接回滚的方式,适合整个事务都需要撤销的场景,不需要额外设置保存点,操作更简单直接。

下面我们继续:

接下来我们演示事务提交后的持久性,也就是事务一旦提交,就无法再回滚的特性。我们用 begin开启一个新事务,同样插入三条数据,此时查询表中已经能看到这三条数据,它们处于事务的临时状态中。然后我们执行 commit 提交事务,提交后数据就被永久写入数据库了,此时再查询表,三条数据依然存在。即使我们再执行 rollback,也无法撤销这些修改,数据不会发生任何变化,这就验证了事务的持久性特性,也说明提交后的事务无法再回滚。

通过这两组对比操作,我们可以明确事务的核心规则:保存点是可选的,它只是为了支持部分回滚而设计的,没有保存点也可以直接回滚整个事务;而 commit 是事务的终点,一旦提交,修改就会永久生效,再也无法通过回滚撤销。这也是事务的 ACID 特性在实际操作中的直接体现,原子性保证了回滚能撤销所有修改,持久性保证了提交后的修改不会丢失。

事务异常验证

下面我们进行事务的异常处理,现在我们来看第一个实验:客户端崩溃后的自动回滚。

我们先在左边的客户端里,用 begin 开启一个事务,此时表中原本有张三、李四、王五三条数据。接着我们插入赵六这条新数据,此时在同一个客户端里查询,能看到四条数据都存在,因为事务还没提交,修改只在当前会话的临时状态里生效。这时候我们模拟客户端突然崩溃,直接关闭这个终端,连接断开。再用另一个客户端查询这张表,会发现表中依然只有原来的三条数据,赵六这条数据消失了。这说明当客户端异常断开时,MySQL 会自动回滚这个未提交的事务,撤销所有修改,这就是事务的自动回滚机制,也是原子性的体现 —— 事务要么全部成功提交,要么在异常时全部撤销,不会留下中间状态的数据。

这里我们开了两个终端,模拟两个客户端 mysql 进程,为什么? 有什么联系吗?

左边和右边开的是两个完全独立的 MySQL 客户端会话,它们各自开启的事务也是相互独立的,并不是同一个事务。并且两个事务操作的是同一张 account 表,但它们的执行过程和状态是完全隔离的,互不影响。开两个终端的目的,核心就是为了演示事务的隔离性和并发性。MySQL 允许多个事务同时操作同一张表,这就是并发场景;而事务的隔离性,就是用来保证这些并发事务之间不会互相干扰,避免出现脏读、不可重复读等数据不一致问题。

接下来是第二个实验:关闭终端后的自动回滚验证。

我们再次开启一个新事务,同样插入赵六这条数据,此时在当前客户端里能看到四条数据。然后我们直接关闭终端,相当于客户端异常断开。之后重新打开客户端查询这张表,会发现表中还是只有原来的三条数据,新插入的赵六又消失了。这个实验和上一个实验的效果完全一致,再次验证了同一个结论:只要事务没有被 commit 提交,无论是主动关闭终端还是客户端崩溃,MySQL 都会自动回滚这个事务,确保数据不会停留在不一致的中间状态。这也是事务的原子性和持久性共同作用的结果,原子性保证了未提交的修改可以全部撤销,而只有提交后的修改才会被永久保存。

我们接着来看事务提交后的持久性验证,这也是事务 ACID 特性中 “持久性” 的直观体现。

在这个实验里,我们先查询 account 表,此时表中只有张三、李四、王五三条数据。接着我们用 begin 手动开启一个事务,插入赵六这条新数据,此时在当前会话中查询,能看到四条数据都存在,因为修改处于事务的临时状态中。关键的一步是执行 commit 提交事务,提交完成后,这条数据就被永久写入数据库了。这时我们模拟客户端突然终止,直接关闭这个终端,再用另一个客户端连接数据库查询 account 表,会发现赵六这条数据依然存在,没有因为客户端断开而消失。

这个实验直接验证了事务的持久性特性:一旦事务被成功提交,数据的修改就会被永久保存到数据库中,无论客户端是否断开、服务器是否重启,数据都不会丢失。同时我们也能看到,手动开启事务后,必须用 commit 手动提交,这和 MySQL 默认的自动提交模式并不冲突,即使修改了自动提交的配置,只要用 begin 或 start transaction 显式开启了事务,就需要手动执行 commit 或 rollback 来结束事务,自动提交模式在显式事务中是暂时失效的。

证明单条SQL与事务的关系

我们接着来看单条 SQL 和事务的关系,以及事务操作的注意事项,这部分能帮我们彻底理解事务在 MySQL 中的底层逻辑。

下面我们进行一组对比试验:

首先,我们先关闭自动提交模式,把 autocommit 设置为OFF,然后开启事务进行操作。

我们先用 begin 启动事务,删除 id=5 的数据,此时查询表中数据,能看到 id=5 的数据已经被删除了,但因为事务还没提交,这个修改只在当前会话中生效。

接着我们执行 commit 提交事务,让修改永久生效。之后我们再删除 id=2 的数据,然后接着直接关闭了客户端,再次连接查询时,会发现 id=2 的数据又恢复了,这说明在自动提交关闭的情况下,未提交的修改会因为客户端断开而被自动回滚,不会永久写入数据库。

继续

然后我们开启自动提交模式,把 autocommit 设置为ON,再执行删除 id=1 的数据操作。这时候即使我们不手动执行 commit,MySQL 也会把这条 SQL 当成一个独立的事务自动提交,修改会直接永久写入数据库。之后我们再关闭客户端重新连接查询,会发现 id=1 的数据已经被永久删除了,无法再回滚恢复。

结论 :

通过这两组对比,我们能清晰看到 autocommit 的作用:

  1. 当 autocommit 为 ON 时,每一条 SQL 都会被自动当成一个事务提交,修改直接持久化;
  2. 当为 autocommit 为 OFF 时,必须手动执行 commit,否则修改不会被永久保存,客户端断开就会自动回滚。
  3. 单条 SQL 语句本身就是一个事务,默认处于自动提交模式,只是平时我们没有显式感知到而已。

事务操作的注意事项

接下来我们看事务操作的注意事项,这些都是实际使用中必须遵守的规则。如果事务没有设置保存点,直接使用 rollback 会回滚到事务开始的状态,前提是事务还没有被提交;一旦事务被 commit 提交成功,就再也无法通过 rollback 回滚,修改会永久生效。保存点的作用就是支持部分回滚,我们可以根据需要回滚到指定的保存点位置,而不是撤销整个事务。同时要注意,只有 InnoDB 引擎支持事务,MyISAM 引擎是不支持事务的,开启事务也无法生效。开启事务可以用 start transaction 或者 begin 两种方式,效果完全一样。从这些例子里,我们已经能直观看到事务的原子性和持久性特性,而隔离性和一致性,会在后续的隔离级别演示中进一步体现。

五、事务隔离级别

什么是事务的隔离性?

MySQL 服务会同时被多个客户端进程访问,每个客户端的操作都是以事务的形式进行的。一个事务往往由多条 SQL 语句组成,它会经历执行前、执行中、执行后三个阶段。原子性保证了我们要么看到事务执行前的状态,要么看到执行后的状态,中间的执行过程对用户是透明的,一旦出问题就能随时回滚。但问题在于,当多个事务同时操作同一张表,甚至同一行数据时,它们的执行过程还是会互相干扰,这时候就需要隔离性来解决问题。隔离性的核心目的,就是让每个事务在执行过程中尽量不受其他事务的干扰,保证事务的执行环境是稳定的。而不同的隔离级别,就是数据库允许事务受到不同程度干扰的规则,隔离级别越高,事务之间的干扰就越小,但性能开销也会相应增加。


我们用一个生活化的故事来理解这个概念。比如我们正在图书馆写一份非常重要的期末论文,这就好比一个事务,我们的目标是从 “没写完” 到 “提交完成”,中间不能被打断。如果没有隔离性,就像我们写论文的时候,旁边的同学一会儿凑过来改我们的句子,一会儿又把我们的草稿纸拿走,我们根本没法顺利完成写作,最终的论文也会变得乱七八糟。而隔离性就像是给我们安排了一个独立的自习隔间,其他人看不到我们写的草稿,也没法修改我们的内容,只能等我们写完提交之后,才能看到最终的成品。这样一来,我们就能不受干扰地完成整个写作过程,就像事务在隔离状态下执行一样,不会被其他并发操作影响,保证了结果的正确性。

这个故事里,我们的写作过程就像事务的执行阶段,而隔离性就像这个独立的隔间,它保证了我们在完成论文之前,外界看不到我们的中间状态,也无法干扰我们的操作,只有等我们最终提交(也就是事务提交),大家才能看到完整的结果。这就是事务隔离性最直白的含义:让每个事务都在自己的 “时间线” 里执行,只能看到自己开始前和提交后的状态,不受其他事务的干扰。

隔离级别

我们接着来讲事务的四个隔离级别,它们是隔离性在实际应用中的具体体现,不同级别对应了事务之间不同程度的干扰与保护。

首先,隔离级别的本质,就是数据库为了平衡并发性能和数据一致性,提供的不同 “保护强度” 选项。隔离级别越低,事务之间的干扰就越多,性能越高;隔离级别越高,干扰越少,性能开销也越大。

1.读未提交是最低的隔离级别,在这个级别下,所有事务都能看到其他事务还没有提交的修改结果,相当于完全没有隔离性。这种级别会带来所有并发问题,比如脏读、不可重复读和幻读,实际生产中几乎不会使用,我们之前做实验时用它,只是为了方便演示并发现象。

2.读提交是大多数数据库的默认隔离级别,但不是 MySQL 的默认级别。在这个级别下,一个事务只能看到其他事务已经提交的修改,看不到未提交的修改,这就解决了脏读问题,但依然会出现不可重复读。也就是说,同一个事务内,两次相同的查询可能会得到不同的结果,因为其他事务在这两次查询之间提交了修改。

3.可重复读是 MySQL 的默认隔离级别,它保证了同一个事务中,多次读取同一数据时,看到的结果始终是一致的,不会出现不可重复读的问题。但这个级别依然无法解决幻读,也就是同一个事务内,两次范围查询可能会得到不同的结果,因为其他事务在中间插入或删除了数据。

4.串行化是最高的隔离级别,它通过强制事务串行执行,让事务之间不可能互相冲突,从而彻底解决了脏读、不可重复读和幻读所有问题。但这种级别会在读取的每一行数据上都加上共享锁,导致大量的超时和锁竞争问题,性能极低,实际生产中几乎不会使用。

这些隔离级别的实现,底层都是通过锁机制完成的,不同级别使用的锁类型不同,比如表锁、行锁、读锁、写锁,还有间隙锁、Next-Key 锁等。现在我们先从上层使用的角度理解它们的作用,后续再深入底层实现细节。

查看与设置隔离性

我们接着来看如何在 MySQL 中查看和设置事务的隔离级别,这部分是实际使用中非常基础且关键的操作。

查看

首先,我们来看如何查看当前的隔离级别。在 MySQL 中,我们可以通过几个不同的变量来查看,@@global.tx_isolation代表全局的隔离级别配置,它是所有新会话的默认值;@@session.tx_isolation代表当前会话的隔离级别,也就是我们当前连接使用的隔离级别;@@tx_isolation 是 @@session.tx_isolation的简写形式,默认和会话级别的配置一致。全局配置是会话配置的源头,每个新会话开启时,都会用全局配置初始化自己的会话隔离级别,这就是它们之间的关系。通过这些命令,我们可以清晰地看到当前全局和会话分别处于什么隔离级别,方便后续调整。接下来,我们来看如何设置隔离级别。

设置

接下来,我们来看如何设置隔离级别。设置隔离级别的语法是固定的,通过SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL来修改,后面跟上需要的隔离级别,包括读未提交、读提交、可重复读和串行化这四种。我们以设置读提交为例,执行set session transaction isolation level read committed;,这条命令只会修改当前会话的隔离级别,对全局配置没有任何影响。执行之后,我们再查询全局和会话的隔离级别,会发现全局依然是原来的读未提交,而当前会话已经变成了读提交,这就验证了会话级别的修改只会影响自己,不会改变全局配置。如果设置的是GLOBAL级别,那么这个修改会影响所有后续新建的会话,它们都会使用新的全局隔离级别,但当前已经存在的会话不会受到影响。

最后我们来总结一下关键结论。会话级别的隔离级别修改,只对当前连接有效,关闭连接后就会失效,不会影响其他用户;而全局级别的修改,会影响所有新开启的会话,需要谨慎操作。在实际生产中,一般不建议随意修改隔离级别,如果确实需要调整,最好保证所有会话的隔离级别保持一致,避免因为不同会话的隔离级别差异,导致并发场景下出现难以排查的数据一致性问题。

六、事务隔离级别

下面我们进行实验测试这些隔离性:

事务隔离级别 - 读未提交

这里为了满足并发场景: 我们打开两个终端 为什么?

打开两个终端,本质上是为了模拟两个完全独立、并发运行的客户端事务会话,以此还原真实业务中“多用户同时操作同一张表”的并发场景,核心目的有三个。

  • 首先,MySQL 服务在实际生产中,就是同时被多个客户端连接访问的,每个客户端的操作都以独立事务的形式运行,它们可能同时读写同一张表、同一行数据。用两个终端,就是为了模拟这种 “两个独立事务同时操作同一资源” 的并发环境,让我们能直观看到事务之间的交互与影响,而单终端无法实现这种多会话的并发效果。
  • 其次,我们需要验证不同隔离级别下的事务隔离效果。比如读未提交级别,就是要观察 “事务 A 未提交的修改,是否会被事务 B 读到”;而可重复读级别,就是要观察 “事务 B 在事务 A 修改并提交前后,两次读取同一数据的结果是否一致”。这些现象,都必须在两个独立会话中才能复现,单终端里的操作无法体现 “并发事务之间的隔离 / 干扰”。
  • 最后,两个终端的会话是完全独立的,各自维护自己的事务状态、隔离级别和数据视图,互不影响。这样我们就能精准控制其中一个事务执行修改、提交或回滚,同时在另一个事务中观察数据的可见性变化,清晰验证脏读、不可重复读、幻读等并发问题,以及不同隔离级别对这些问题的解决效果。

简单来说,没有两个终端,就没法模拟并发事务,也就没法直观理解隔离性和隔离级别的实际作用。

我们先来验证读未提交这个隔离级别,这也是最低的隔离级别,能直观看到并发事务之间的干扰现象:

为了模拟并发场景,我们打开两个 MySQL 终端,并且把两个终端的隔离级别都设置为读未提交,也就是 READ-UNCOMMITTED,这样两个会话都处于最低的隔离级别,事务之间几乎没有任何隔离保护。

下面同时启动事务:

接着,我们在两个终端里同时用 begin 开启事务,让两个事务处于并发运行的状态,此时两个事务都还没有执行 commit 提交,各自处于执行中的阶段。我们这样做的目的,就是为了测试在没有隔离保护的情况下,两个并发事务的执行过程会不会互相影响,验证原子性和隔离性的表现。

下面来验证一下:

接下来我们进行操作验证,左边的事务先执行插入一条新数据,再更新另一条数据的操作,这两个操作都还没有提交。而右边的事务在左边事务执行的过程中,多次查询 account 表,结果发现左边事务还没提交的插入和更新操作,右边的事务都能实时看到。这就是读未提交级别的核心特点:一个事务可以读取到其他事务还没有提交的修改结果,这种现象也被称为脏读。也就是说,左边事务的中间状态,完全暴露给了右边的事务,隔离性在这里几乎不存在。

接下来回滚:

最后,左边的事务执行了 rollback 回滚,撤销了所有修改,此时再查询 account 表,数据又回到了最开始的状态,之前插入和修改的数据都消失了。这也说明,在左边事务回滚之后,右边事务之前读到的那些数据都是无效的、临时的,这种读取了其他事务未提交且最终被回滚的数据的情况,就是脏读带来的问题,也是读未提交级别最大的缺陷。

结论 :

读未提交最直接的特征,就是一个事务还没有提交的修改,会被其他事务直接看到。这就意味着事务之间几乎没有任何隔离,一个事务的执行过程对其他事务是完全透明的,没有任何保护机制。这种级别的实现几乎没有加锁,所以执行效率很高,不会有锁竞争带来的性能开销,但也正因为没有任何隔离,它会带来大量并发问题,其中最典型的就是脏读,实际生产中几乎不会使用这个级别。

脏读就是读未提交级别下最典型的不合理现象,它指的是一个事务在执行过程中,读到了另一个事务还没有提交的修改数据。比如我们之前的实验里,左边事务插入了一条数据但还没提交,右边事务就已经读到了这条数据,而如果左边事务最后执行了回滚,这条数据就会消失,那么右边事务读到的就是一条无效的、不存在的数据。这种情况会导致业务逻辑出现严重错误,比如转账场景中,事务 A 扣了钱但还没提交,事务 B 读到了 A 的余额变化,就会基于错误的数据做出判断,最终导致数据不一致。

所以,读未提交虽然性能最高,但它带来的脏读问题是无法接受的,严重破坏了数据的一致性和可靠性,因此实际生产中几乎不会采用这个隔离级别,它更多是作为教学场景,用来演示脏读现象和隔离级别的作用。

事务隔离级别 - 读提交

我们接着来看读提交这个隔离级别,它解决了读未提交的脏读问题,是大多数数据库的默认隔离级别,我们还是用双终端并发实验来理解它的特点。

首先,我们先把两个终端的隔离级别都改成读提交,通过select @@tx_isolation可以看到当前会话的隔离级别已经变成了READ-COMMITTED。两个终端的隔离级别都是读提交

接着,我们在两个终端同时开启事务,让它们处于并发运行的状态,此时两个事务都还没有提交。左侧事务执行插入新数据和更新已有数据的操作,这些修改在左侧会话中已经能看到,但此时右侧事务多次查询同一张表,始终只能看到修改前的原始数据,看不到左侧事务未提交的任何操作结果,这说明读提交级别下,未提交的事务修改对其他事务是不可见的,脏读问题已经被解决了。

接下来,左侧事务执行 commit 提交事务,把所有修改永久写入数据库。此时右侧事务再次查询同一张表,就能看到左侧事务提交后的修改结果,新插入的数据和更新后的字段值都已经能正常显示了。即使右侧事务之后也执行了提交,它看到的依然是这些已经提交的修改结果,不会出现数据异常。

通过这个实验,我们可以得出读提交隔离级别的核心结论:两个并发执行的事务中,一个事务执行的增删改操作,在未提交之前,其他事务完全看不到;只有当这个事务提交之后,其他事务才能看到它的修改结果。也就是说,读提交级别保证了事务只能读取到其他事务已经提交的数据,不会读到未提交的脏数据。这种级别下,事务看到的数据不是实时更新的最新状态,而是其他事务提交后的状态,这虽然会带来不可重复读的问题,但解决了脏读这个严重的数据一致性风险,是大多数业务场景下比较均衡的选择。


不可重复读现象

我们接着来看读提交隔离级别下,一个新的并发现象 —— 不可重复读,这也是读提交级别无法解决的问题。

在这个实验里,右侧的事务还处于开启状态,没有执行 commit 提交。它第一次执行 select * from account; 查询时,只能看到李四和赵六两条数据,这是左侧事务提交前的原始状态。但当左侧事务提交了它的修改之后,右侧事务再次执行完全相同的 select 语句,结果却变成了三条数据,还看到了新插入的张三和更新后的李四信息。这种在同一个事务内,两次执行完全相同的查询语句,却得到了不同结果的现象,就叫做不可重复读。

不可重复读的问题在于,同一个事务内的数据视图发生了变化,这会导致业务逻辑出现混乱。比如我们在一个事务里,第一次查询账户余额是 1000 元,基于这个结果进行了后续计算,还没等事务提交,其他事务修改并提交了余额,第二次查询就变成了 800 元,两次计算的结果就会不一致,最终可能导致业务逻辑出错。所以,不可重复读也是一个需要避免的并发问题,这也是 MySQL 默认采用可重复读隔离级别的原因之一,可重复读级别会保证同一个事务内多次读取同一数据的结果始终一致,从而解决不可重复读的问题。

事务隔离级别 - 可重复读

我们接着来看可重复读这个隔离级别,它是 MySQL 的默认隔离级别,解决了读提交带来的不可重复读问题。

首先,可重复读的核心定义是,在同一个事务的执行过程中,多次读取同一数据,得到的结果始终是一致的,不会因为其他事务提交了修改而发生变化。也就是说,一个事务一旦开始,它在运行期间看到的数据视图就被固定下来了,其他事务的提交不会对它产生影响,这样就避免了同一个事务内两次查询结果不同的不可重复读问题。

举例:

首先,我们通过 select @@tx_isolation 确认当前会话的隔离级别是 REPEATABLE-READ,也就是可重复读,两个终端都设置为这个级别。

初始状态下,表中有张三、lisi、赵六三条数据,左侧事务开启后,第一次查询表,看到的就是这三条数据。

然后,右侧事务对表进行了修改,插入了新数据田七,同时更新了 lisi 的数据为李四,并且执行了 commit 提交事务。此时,左侧事务还处于开启状态,没有提交,它再次查询表看到的依然是最初的三条数据,没有任何变化,完全不受右侧事务提交的影响。只有当左侧事务自己执行了 commit 提交之后,再查询表才能看到右侧事务提交后的最新数据,也就是包含田七和更新后李四的四条数据。

这个实验的现象完美体现了可重复读的特点:在事务运行期间,即使其他事务提交了修改,当前事务也不会看到这些变化,始终保持事务开始时的数据视图,从而保证了同一个事务内多次读取同一数据的结果是一致的。它解决了不可重复读问题,让事务的执行过程更加稳定,这也是 MySQL 选择它作为默认隔离级别的原因。不过,可重复读依然无法解决幻读问题,这也是我们接下来要讲的内容。

幻读

首先我们来理解什么是幻读。幻读发生在可重复读隔离级别下,核心表现是同一个事务内,两次执行相同的范围查询,结果却出现了之前没有的新记录,就像产生了幻觉一样。比如终端 A 的事务在表中插入了一条新数据并commit提交,而终端 B 的事务还处于开启状态,在可重复读的保护下,它的普通查询看不到这条新数据,符合可重复读的特点。但在标准的可重复读定义中,由于数据库的隔离性是通过对现有数据加锁实现的,而待插入的数据原本不存在,锁机制无法屏蔽这类新增操作,这就可能导致在某些特殊场景下,终端 B 的事务会读到这条新插入的数据,让两次查询结果出现差异,这就是幻读。需要注意的是,标准的可重复读会存在幻读问题,但 MySQL 的可重复读级别通过 Next-Key 锁(间隙锁 + 行锁)的机制,在很大程度上解决了幻读问题,只是底层实现比较复杂,我们可以先从概念上理解它的含义。

接下来我们来说说为什么 MySQL 会把可重复读选为默认隔离级别。首先,可重复读解决了读未提交的脏读问题,也解决了读提交的不可重复读问题,在事务执行过程中,同一个事务内的多次读取结果始终一致,保证了数据视图的稳定性,避免了业务逻辑中因为数据变化导致的错误。其次,它的性能开销远低于串行化隔离级别,不需要对所有数据加锁,也不会导致严重的锁竞争和超时问题,能在数据一致性和并发性能之间取得很好的平衡。最后,MySQL 还在可重复读级别下额外实现了 Next-Key 锁机制,解决了标准可重复读的幻读问题,让它几乎满足了绝大多数业务场景的并发安全需求,因此成为了 MySQL 的默认隔离级别。

事务隔离级别 - 串行化

我们接着来看事务隔离级别的最后一级 —— 串行化,这是隔离级别中最高的一级,也是性能最低的一级。

首先,串行化隔离级别的核心逻辑是对所有的读写操作都加锁,强制事务按照串行的方式依次执行,杜绝并发事务之间的互相干扰,从根本上解决脏读、不可重复读和幻读所有问题。但也正因为所有操作都要排队执行,没有任何并发,它的效率会变得非常低,在实际生产中几乎不会被采用。

示例:

首先,两个终端的隔离级别都被设置为了 SERIALIZABLE 串行化。

右侧终端先开启了事务并执行查询操作,这时候它会给表加上共享锁。接着左侧终端开启事务并尝试执行删除操作,却发现操作一直卡在那里没有反应,右侧终端多次查询同一张表,数据也始终没有变化,这是因为左侧的删除操作需要获取排他锁,而右侧事务还持有共享锁,两者冲突,左侧操作只能等待。

直到右侧事务执行了 commit 提交,释放了共享锁之后,左侧终端才终于拿到了锁,完成了删除操作,整个过程耗时近 15 秒,这就是串行化隔离级别带来的性能开销,仅仅是一个简单的删除操作,就因为锁等待消耗了大量时间。

这个实验直观地展示了串行化隔离级别的特点:它通过加锁的方式,让事务必须排队执行,一个事务没提交释放锁,其他事务就只能一直等待,以此实现完全的隔离性。这种方式虽然解决了所有并发问题,但也牺牲了数据库的并发性能,尤其是在业务量大、事务多的场景下,锁等待会导致大量请求超时,因此实际生产中几乎不会使用这个级别,更多是作为理论上的最高隔离级别存在。

串行化原理:

首先看串行化的工作原理:

在串行化隔离级别下,当客户端 A 以串行化模式执行 select 查询时,数据库管理系统会自动为它操作的 account 表加上共享锁。此时如果客户端 B 要执行 update 操作,就需要获取排他锁,而排他锁和客户端 A 持有的共享锁是互斥的,客户端 B 无法立刻拿到锁,只能进入等待队列排队。如果客户端 A 一直不提交事务释放锁,客户端 B 就会一直等待,甚至可能出现超时错误。只有当客户端 A 提交事务、释放锁之后,客户端 B 才能拿到锁并执行操作,这就是串行化的核心逻辑:通过加锁强制事务排队执行,让并发事务变成串行执行,以此彻底避免所有并发问题。

总结:

接下来我们对四个隔离级别做一个完整的总结:

隔离级别的设置本质上是在数据安全性和数据库并发性能之间做平衡,隔离级别越高,数据安全性就越强,但数据库的并发性能也会越低,实际业务中我们需要根据场景选择合适的平衡点。读未提交是最低的隔离级别,它不加任何锁,性能最高,但会同时存在脏读、不可重复读和幻读三种问题,实际生产中几乎不会使用。读提交解决了脏读问题,事务只能读取其他事务已经提交的数据,但依然会出现不可重复读和幻读,它是大多数数据库的默认隔离级别,MySQL 默认不采用它。可重复读是 MySQL 的默认隔离级别,它解决了脏读和不可重复读问题,并且通过 Next-Key 锁机制在 MySQL 中也解决了幻读问题,它不需要加锁,能在安全性和性能之间取得很好的平衡,一般情况下不建议修改这个默认设置。串行化是最高的隔离级别,它通过加锁强制事务串行执行,彻底解决了所有并发问题,但会严重降低并发性能,实际生产中几乎不会使用。

我们还要区分一下不可重复读和幻读的不同点。不可重复读的重点在于修改和删除操作,同一个事务内,两次读取同一条记录,结果值发生了变化。幻读的重点在于新增操作,同一个事务内,两次执行相同的范围查询,结果返回的记录数不一样,就像凭空多出来了数据一样。另外,事务之间的互相影响,主要发生在事务并行执行、都还没有提交的时候,这时候不同隔离级别的保护效果差异会表现得最明显。

七、总结

本文系统介绍了MySQL事务的核心概念和特性。事务是数据库操作的安全单元,通过ACID特性(原子性、一致性、隔离性、持久性)保证数据安全可靠。文章详细解析了事务的四大隔离级别:读未提交(存在脏读)、读提交(解决脏读但存在不可重复读)、可重复读(MySQL默认级别,解决不可重复读和幻读)、串行化(最高隔离但性能最差)。通过实验演示了各隔离级别下的并发问题及解决方案,强调了事务提交与回滚机制的重要性,并指出InnoDB引擎支持事务而MyISAM不支持。最后建议在保证数据安全与系统性能间选择合适隔离级别,通常可重复读是最佳平衡点。

谢谢大家的观看!