MySQL数据库 (十四) MySQL事务(上),事务的四大特性,事务的操作方式,事务的隔离级别
目录一、引入事务二、事务什么是事务?事务的属性事务的版本支持三、事务的提交事务的提交方式四、事务常见操作方式提前准备:事务的正常操作事务异常验证证明单条 SQL 与事务的关系事务操作的注意事项五、事务隔离级别什么是事务的隔离性?隔离级别查看与设置隔离性六、事务隔离级别事务隔离级别 - 读未提交事务隔离级别 - 读提交事务隔离级别 - 可重复读事务隔离级别 - 串行化总结:七、总结本篇文章我们来开启一个全新的话题MySQL 事务。在这之前我们聊的都是索引它解决的是 “怎么查得快” 的问题而事务要解决的是 “数据怎么保证安全可靠” 的问题。一、引入事务我们先来看图里这个经典的火车票售票场景就能明白事务存在的必要性。假设系统里只剩 1 张票了数据表里的 nums 字段值为 1。这时候客户端 A 和客户端 B 同时发起了买票请求客户端 A 先查询发现票数 nums 0于是准备卖票。但它还没来得及执行 update nums nums - 1 更新数据库。几乎在同一时间客户端 B 也查询了这张票发现 nums 同样大于 0于是也准备卖票。客户端 A 执行更新把票数减为 0。客户端 B 再执行更新也把票数减为 0。这样一来一张票就被卖给了两个人数据出现了严重的错误。这就是没有事务控制时并发操作可能导致的问题。要解决这个问题我们就需要引入事务的概念让数据操作满足四个关键特性也就是我们常说的 ACID原子性Atomicity买票的整个过程必须是一个不可分割的整体要么全部成功要么全部失败。不能出现 “票卖出去了但库存没更新” 或者 “库存扣了但订单没生成” 的中间状态。一致性Consistency事务执行前后数据必须始终处于合法的状态。比如上面的例子最终票数要么是 1都没买到要么是 0被其中一个人买到不能出现负数也不能一张票卖给两个人。隔离性Isolation多个客户端同时操作时它们之间不能互相干扰。A 的事务在提交之前B 不应该看到 A 还没更新的中间状态数据这样就不会出现 “看到票还在但实际已经被卖了” 的情况。持久性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 autocommit0; 来关闭自动提交切换到手动模式用 set autocommit1; 来重新开启自动提交切换到默认模式。接下来我们说自动提交模式也就是 autocommitON 的情况这是 MySQL 的默认配置。在这种模式下我们执行的每一条 SQL 语句都会被 MySQL 当成一个独立的事务来处理。比如执行一条 update 修改数据MySQL 会立刻自动提交这个事务修改会直接永久写入数据库没有机会再用 rollback 把它撤销回来。这种模式适合执行单条的、不需要回滚的简单操作比如查询、一次性的修改语句因为它不需要你额外做任何提交操作用起来很方便。然后是手动提交模式也就是 autocommitOFF 的情况。在这种模式下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 启动事务删除 id5 的数据此时查询表中数据能看到 id5 的数据已经被删除了但因为事务还没提交这个修改只在当前会话中生效。接着我们执行 commit 提交事务让修改永久生效。之后我们再删除 id2 的数据然后接着直接关闭了客户端再次连接查询时会发现 id2 的数据又恢复了这说明在自动提交关闭的情况下未提交的修改会因为客户端断开而被自动回滚不会永久写入数据库。继续然后我们开启自动提交模式把 autocommit 设置为ON再执行删除 id1 的数据操作。这时候即使我们不手动执行 commitMySQL 也会把这条 SQL 当成一个独立的事务自动提交修改会直接永久写入数据库。之后我们再关闭客户端重新连接查询会发现 id1 的数据已经被永久删除了无法再回滚恢复。结论 :通过这两组对比我们能清晰看到 autocommit 的作用当 autocommit 为 ON 时每一条 SQL 都会被自动当成一个事务提交修改直接持久化当为 autocommit 为 OFF 时必须手动执行 commit否则修改不会被永久保存客户端断开就会自动回滚。单条 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不支持。最后建议在保证数据安全与系统性能间选择合适隔离级别通常可重复读是最佳平衡点。谢谢大家的观看!