2026/10/28 10:17:11

MySQL 基础篇(十一):事务与隔离级别

MySQL 基础篇(十一):事务与隔离级别 目录本文内容概要一、认识事务1.1 什么是事务1.2 为什么需要事务1.3 事务的四大特性ACID1.3.1 原子性Atomicity1.3.2 一致性Consistency1.3.3 隔离性Isolation1.3.4 持久性Durability二、事务的基本操作2.1 MySQL 的自动提交机制2.2 开启事务2.3 提交事务2.5 保存点2.6 实战模拟银行转账2.6.1 创建测试数据2.6.2 执行转账事务2.6.3 验证事务回滚三、并发事务带来的问题3.1 脏读3.2 不可重复读3.3 幻读3.4 丢失更新四、事务的隔离级别4.1 MySQL 的四种事务隔离级别4.2 读未提交READ UNCOMMITTED4.3 读已提交READ COMMITTED4.4 可重复读REPEATABLE READ4.5 串行化SERIALIZABLE4.6 查看与设置事务隔离级别4.6.1 查看事务隔离级别4.6.2 设置会话级别的事务隔离级别4.6.3 设置全局级别的事务隔离级别本文内容概要本文主要介绍 MySQL 事务的基本概念、基本操作以及事务隔离级别帮助读者理解事务在数据库操作中的作用以及并发事务可能带来的问题。本文主要内容如下认识事务介绍事务的基本概念、使用事务的原因以及 ACID 四大特性。事务的基本操作介绍自动提交机制、事务的开启、提交、回滚与保存点并通过银行转账案例进行演示。并发事务带来的问题介绍脏读、不可重复读、幻读和丢失更新分析这些问题的产生原因。事务的隔离级别介绍读未提交、读已提交、可重复读和串行化四种隔离级别并通过双会话实验观察不同隔离级别下的事务行为。隔离级别的查看与设置介绍如何查看和设置 MySQL 全局及会话级别的事务隔离级别。通过本文的学习读者应该能够掌握 MySQL 事务的基本使用方式理解常见的并发事务问题并根据实际业务需求选择合适的事务隔离级别。至于 InnoDB 的 MVCC 机制、Undo Log、Read View 以及数据记录的隐藏属性等底层实现原理将在后续文章中单独介绍。一、认识事务1.1 什么是事务在学习 MySQL 的过程中我们需要经常对数据库中的数据进行 CURD。在实际业务中有些操作并不是独立完成的而是需要多个操作相互配合共同完成一个业务目标。例如在银行转账业务中假设用户 A 需要向用户 B 转账 100 元则需要完成以下两个操作将用户 A 的账户余额减少 100 元。将用户 B 的账户余额增加 100 元。这两个操作共同构成了一次完整的转账业务。但是如果在执行第一个操作之后数据库突然发生异常导致第二个操作没有执行那么用户 A 的余额已经减少而用户 B 的余额却没有增加。这就会造成数据不一致的问题。那么如何保证这两个操作能够作为一个整体执行呢这就需要使用事务。事务是数据库管理系统提供的一种机制用于将多个数据库操作组织成一个逻辑上的整体。事务中的操作要么全部成功要么全部失败从而保证数据的一致性。对于上述转账业务我们可以将扣款和入账两个操作放在同一个事务中如果两个操作都成功执行就提交事务使修改结果生效。如果执行过程中发生异常就回滚事务撤销本次事务中尚未提交的修改。通过这种方式我们就可以避免只完成部分操作而导致的数据不一致问题。需要注意的是事务并不意味着多个 SQL 语句会自动执行。我们仍然需要编写相应的 SQL 语句而事务负责管理这些操作之间的整体性。1.2 为什么需要事务理解了事务的基本概念之后我们再来思考一个问题如果没有事务会发生什么仍然以银行转账为例假设用户 A 的余额为 1000 元用户 B 的余额为 500 元现在 A 向 B 转账 100 元。转账前账户状态如下用户账户余额A1000 元B500 元总余额1500 元正常情况下转账完成之后账户状态应该变为用户账户余额A900 元B600 元总余额1500 元可以看到转账只是改变了资金在两个账户之间的分配并没有改变两个账户的总余额。但是如果没有事务的管理假设 A 的扣款操作已经成功而 B 的入账操作因为某种原因失败那么数据库中的状态就会变成用户账户余额A900 元B500 元总余额1400 元原本应该保持不变的总余额减少了 100 元显然不符合转账业务的要求。在实际应用中数据库操作可能受到程序异常、数据库连接中断、服务器故障等因素的影响。如果多个操作之间缺乏有效的事务管理就可能出现只完成部分操作的情况。事务的作用就是将这些相关操作作为一个整体进行管理尽可能避免部分操作成功、部分操作失败所造成的数据不一致问题。因此事务特别适用于以下场景银行转账扣款与入账必须相互配合。订单处理创建订单、扣减库存等操作需要协调执行。账户余额变更转账、充值、退款等操作需要保证数据正确。业务数据更新多张数据表之间的修改需要满足统一的业务规则。需要说明的是事务主要负责保证事务操作的原子性、隔离性等属性并不能自动保证所有业务逻辑都正确。例如如果程序本身计算出了错误的转账金额事务也不会自动纠正这个错误。1.3 事务的四大特性ACID为了保证事务能够正确执行数据库事务通常具有四个重要特性分别是原子性Atomicity、一致性Consistency、隔离性Isolation和持久性Durability合称ACID。1.3.1 原子性Atomicity原子性指的是事务中的操作要么全部完成要么全部不生效不存在只成功一部分的最终结果。继续以银行转账为例扣款和入账属于同一个事务。如果两个操作都成功执行那么事务可以正常提交如果其中一个操作发生异常事务可以回滚撤销本次事务中已经执行但尚未提交的修改。这样就可以避免只扣款、不入账的情况。在 MySQL 的 InnoDB 存储引擎中原子性主要依靠 Undo Log 等机制实现。当事务需要回滚时数据库可以利用相关信息撤销事务所做的修改。1.3.2 一致性Consistency一致性指的是事务执行前后数据库都应该满足相应的完整性约束和业务规则。例如在银行转账业务中如果我们规定账户余额不能为负数那么每次转账操作完成后都应该满足这一规则。对于前面介绍的 A 向 B 转账 100 元的业务在正常转账且不考虑其他并发资金变动的情况下两个账户的总余额应当保持不变。事务执行前两个账户的总余额为 1500 元事务正常完成后总余额仍然为 1500 元。这就是该业务需要满足的一致性要求。需要注意一致性并不是仅仅依靠事务就能自动实现的。它还依赖于正确的业务逻辑、数据库约束以及原子性、隔离性和持久性等特性的共同保障。例如如果程序允许用户转账超过账户余额的金额那么即使整个事务成功提交也可能违反业务规则。因此程序仍然需要对余额进行合理校验。1.3.3 隔离性Isolation隔离性指的是多个事务并发执行时一个事务的执行不应该受到其他事务不合理的干扰。假设两个事务同时读取同一个账户的余额并根据读取结果进行修改。如果缺乏适当的并发控制就可能出现相互覆盖修改结果等问题。例如账户余额为 1000 元两个事务分别准备扣除 100 元和 200 元。如果两个事务都基于最初读取的余额进行计算并且后写入的结果覆盖了先写入的结果那么最终余额就可能不符合预期。为了控制并发事务之间的相互影响MySQL 提供了不同的事务隔离级别例如读未提交、读已提交、可重复读和串行化。不同隔离级别对并发事务的限制程度不同能够避免的问题也有所区别。后续章节将详细介绍脏读、不可重复读、幻读等并发问题以及 MySQL 如何通过 MVCC 和锁机制实现事务隔离。1.3.4 持久性Durability持久性指的是事务一旦成功提交其修改结果就应该得到持久保存即使数据库随后发生故障也应该能够通过恢复机制保证已提交事务的结果。例如用户 A 向用户 B 转账 100 元数据库已经成功提交事务。如果此时服务器突然断电那么在数据库恢复之后这笔已经提交的转账仍然应该得到保留。在 MySQL 的 InnoDB 存储引擎中Redo Log 是保障事务持久性的重要机制。数据库可以利用相关日志信息在发生故障后恢复已经提交但尚未完全写入数据文件的修改。当然持久性依赖于数据库的日志配置、存储设备以及故障恢复机制等条件并不意味着任何硬件损坏或所有灾难场景下都绝对不会丢失数据。二、事务的基本操作2.1 MySQL 的自动提交机制在使用 MySQL 执行 SQL 语句时大家可能会存在一个疑问我们并没有显示开启事务为什么 INSERT、UPDATE、DELETE 等语句之后数据依然会变化这是因为MySQL 默认开启自动提交机制单条 SQL 语句会被当成一个事务我们可以通过以下 SQL 语句来查看当前会话的自动提交状态mysql select autocommit; -------------- | autocommit | -------------- | 1 | -------------- 1 row in set (0.00 sec)其中1 表示开启自动提交 0 表示关闭自动提交当自动提交开启时每条独立的 SQL 语句会作为一个事务执行并在语句成功执行后自动提交。此外我们可以通过以下语句修改当前会话的自动提交设置mysql select autocommit; -------------- | autocommit | -------------- | 1 | -------------- 1 row in set (0.01 sec) mysql set autocommit 0; Query OK, 0 rows affected (0.00 sec) mysql select autocommit; -------------- | autocommit | -------------- | 0 | -------------- 1 row in set (0.00 sec) mysql set autocommit 1; Query OK, 0 rows affected (0.00 sec) mysql select autocommit; -------------- | autocommit | -------------- | 1 | -------------- 1 row in set (0.00 sec)需要注意set autocommit 修改的是当前会话的设置。关闭自动提交后SQL 语句的数据修改不会像默认模式那样自动提交需要手动 COMMIT 提交事务或者 ROLLBACK 回滚事务。2.2 开启事务在实际业务中如果一个业务需要执行多条 SQL 语句并且要求这些 SQL 作为一个整体成功或者失败那么仅依靠默认的自动提交机制就不够了。此时我们需要显示开启事务将多条 SQL 语句组织在同一个事务中再提交。在 MySQL 中常见开启事务的方式有两种start transaction; begin;事务开启之后我们就可以在其中执行多条 SQL 语句。这些语句的修改可以在事务结束前统一提交也可以通过回滚撤销。start transaction; update account set balance balance - 100 WHERE id 1; update account set balance balance 100 WHERE id 2;上述代码开启了一个事务并依次执行两条 SQL 语句。此时我们还没有提交事务因此这些修改尚未通过事务提交正式生效。需要注意的是事物的修改并不意味着其他事务完全看不到此事务做出的相关数据变化。其他事务能否读取到这些修改取决于事务隔离级别及具体的读取方式。另外事务的行为还与存储引擎有关。MySQL 中的 InnoDB 支持事务而 MyISAM 不支持事务回滚等完整的事务管理能力。因此在实际使用事务时需要确保相关数据表使用支持事务的存储引擎。2.3 提交事务事务中的操作执行完成并且满足业务要求和预期结果时我们可以使用 COMMIT 提交事务commit;提交事务意味着确认本次事务的修改使其按照数据库的事务提交机制正式生效。start transaction; update account set balance balance - 100 WHERE id 1; update account set balance balance 100 WHERE id 2; commit;在上述代码中事务依次完成扣款和入账操作最后通过 COMMIT 提交事务。如果两个更新操作都执行成功并且满足相应的业务条件那么事务就可以正常提交。对于 InnoDB 而言成功提交的事务还需要满足持久性要求即在数据库故障恢复后已提交事务的修改应当得到保留。因此COMMIT 可以理解为确认当前事务的修改并结束当前事务。2.4 回滚事务如果事务执行过程中发现异常或者某些操作不满足业务我们可以使用 ROLLBACK 回滚事务。rollback;回滚操作会直接撤销当前事务尚未提交的修改并结束当前事务。begin; update account set balance balance - 100 where id 1; -- 查看修改后的余额 SELECT * FROM account where id 1; -- 撤销本次事务的修改 rollback; -- 再次查看余额 SELECT * FROM account where id 1;假设账户 A 原来的余额为 1000 元那么执行 UPDATE 之后在当前事务中查询到的余额为 900 元。但是由于我们执行了 ROLLBACK这次修改会被撤销。再次查询时账户余额将恢复为事务开始前的 1000 元。需要注意的是并不是任何一条 SQL 执行失败MySQL 会回滚事务。MySQL 回滚事务是使用者进行 ROLLBACK 或者该事务未被 COMMIT 才会回滚。当事务中出现 SQL 语句的执行失败时例如语法错误等MySQL 并不会进行自动回滚事务。2.5 保存点ROLLBACK 通常用于回滚整个尚未提交的事务COMMIT 用于提交事务。那么如果一个事务包含多个操作我们只想撤销一部分操作而保留之前已经完成的操作此时就需要使用保存点。保存点可以在事务执行过程中设置一个回滚位置使我们能够将事务回滚到指定位置而不必直接撤销整个事务。begin; update account set balance balance - 100 where id 1; -- 创建保存点 savepoint sp1; update account set balance balance 100 where id 2; -- 撤销保存点之后的修改 rollback to savepoint sp1; -- 最终撤销整个事务 rollback;在上述代码中首先开启事务并将账户 A 的余额减少 100 元。创建保存点 sp1记录当前事务的回滚位置。将账户 B 的余额增加 100 元。执行 rollback to savepoint sp1撤销保存点之后的修改但保留保存点之前的修改。最后执行 rollback撤销整个事务中的剩余修改。这里的转账操作仅用于演示保存点的语法实际业务中不能将只完成一半的转账操作提交。需要注意ROLLBACK TO SAVEPOINT 并不会结束整个事务而普通的 ROLLBACK 会回滚整个事务并结束它。另外保存点只是事务内部的回滚标记并不是一次独立提交。即使已经创建保存点事务最终仍然可以整体回滚。2.6 实战模拟银行转账理解了事务的基本操作之后我们通过一个完整案例将前面介绍的知识串联起来。假设存在一张账户表 account其中包含账户编号、账户名称和账户余额。2.6.1 创建测试数据首先创建账户表CREATE TABLE account ( id INT PRIMARY KEY, name VARCHAR(20) NOT NULL, balance DECIMAL(10, 2) NOT NULL ) ENGINE InnoDB;然后插入两条测试数据INSERT INTO account (id, name, balance) VALUES (1, A, 1000.00), (2, B, 500.00);查询初始数据SELECT * FROM account;结果如下idnamebalance1A1000.002B500.00现在我们让账户 A 向账户 B 转账 100 元。2.6.2 执行转账事务START TRANSACTION; -- A 账户扣款 UPDATE account SET balance balance - 100 WHERE id 1 AND balance 100; -- B 账户入账 UPDATE account SET balance balance 100 WHERE id 2; -- 确认操作结果满足业务要求后提交 COMMIT;执行完成后再次查询账户数据SELECT * FROM account;结果如下idnamebalance1A900.002B600.00可以看到A 的余额减少了 100 元B 的余额增加了 100 元两个账户的总余额依然是 1500 元。通过显式事务我们将扣款和入账两个操作组织在一起避免它们在默认自动提交模式下被分别提交。不过上述 SQL 示例为了展示事务的基本用法省略了完整的业务校验。在真实项目中程序还应该检查扣款和入账语句是否影响了预期的记录数并在任一操作失败或业务条件不满足时回滚整个事务。否则即使使用了事务也不能保证业务逻辑一定正确。2.6.3 验证事务回滚接下来我们再模拟一次失败的转账操作。START TRANSACTION; UPDATE account SET balance balance - 100 WHERE id 1; -- 假设此时发现业务条件不满足 ROLLBACK;再次查询账户数据SELECT * FROM account;如果这期间没有其他事务修改账户数据那么查询结果仍然是idnamebalance1A900.002B600.00这是因为本次事务的扣款操作并没有提交而是通过 ROLLBACK 撤销了。三、并发事务带来的问题在上一章中我们详细介绍了 MySQL 事务的基本操作包括事物的开启、提交、回滚以及保存点。但是在实际业务中往往会有多个用户同时访问数据库。如果多个事务同时执行那么它们之间可能相互影响产生并发问题。3.1 脏读脏读是指一个事务读取到了另一个事务尚未提交的数据。了解为什么被称为 “脏” 读呢因为另一个事务尚未提交其修改结果最终可能被回滚。如果当前事务读取了这些数据那么它读取到的就是一份永远不会正式生效的数据。下面通过两个并发事务进行演示假设账户 A 的初始余额为 1000 元事务 T1START TRANSACTION; UPDATE account SET balance 500 WHERE id 1; -- 暂时不提交事务事务 T2--为了演示脏读我们需要将隔离级别设置为读未提交 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; START TRANSACTION; SELECT balance FROM account WHERE id 1;此时会话二可能查询到balance500.00但是账户 A 的余额修改尚未提交。接下来会话一执行ROLLBACK;由于事务 T1 回滚了账户 A 的余额又恢复为 1000 元。这意味着会话二之前读取到的 500 元实际上并没有成为最终生效的数据。这就是脏读。3.2 不可重复读不可重复读是指同一个事务在前后两次读取同一条记录时得到了不同的结果。需要注意的是这里强调的是同一个事务对同一条记录进行多次读取。与脏读不同不可重复读是因为另一个事务修改了相关数据并且提交了修改结果。假设账户 A 的初始余额为 1000 元。为了方便观察我们将会话一设置为读已提交隔离级别。事务 T1SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; START TRANSACTION; SELECT balance FROM account WHERE id 1;第一次查询结果为balance1000.00此时事务 T1 暂时不提交。事务 T2UPDATE account SET balance 800 WHERE id 1;在默认自动提交开启的情况下这条语句成功执行后其修改会自动提交。此时数据库中的账户余额已经变为 800 元。接下来会话一再次执行相同的查询SELECT balance FROM account WHERE id 1;第二次查询结果为balance800.00可以看到事务 T1 在自身事务尚未结束的情况下前后两次读取同一条记录得到了不同的结果。这就是不可重复读。最后会话一执行COMMIT;结束事务。为什么会出现不可重复读这与 MySQL 的隔离级别是强相关的在读已提交的隔离级别下并发中的事务可以读取到其他事务提交的数据。3.3 幻读幻读是指同一个事务按照相同的查询条件多次查询符合条件的记录时结果集中的记录数量或记录集合发生了变化。与不可重复读相比幻读更强调查询条件对应的结果集发生了变化。例如我们执行以下查询SELECT * FROM account WHERE balance 600;第一次查询时数据库中有两条记录满足条件。如果在两次查询之间另一个事务插入了一条余额为 700 元的新账户记录并提交那么再次执行相同的查询时就可能多出一条记录。这就是典型的幻读场景。对比维度不可重复读幻读关注重点同一条记录的内容发生变化满足条件的记录集合发生变化典型原因其他事务修改并提交已有记录其他事务插入新记录或修改记录使其满足查询条件典型表现同一账户余额前后不同相同查询条件返回的记录集合不同例如第一次查询账户 A 的余额为 1000 元第二次变成 800 元属于不可重复读。第一次查询余额大于等于 600 元的账户有两条第二次变成三条属于典型的幻读。这里需要强调 MySQL InnoDB 的实现细节在可重复读隔离级别下普通的快照读会基于事务的 Read View 读取一致的数据版本因此同一个事务重复执行查询时是不存在幻读的情况的。3.4 丢失更新丢失更新是指多个事务并发修改同一份数据时一个事务的修改结果被另一个事务覆盖导致之前的更新没有体现在最终结果中。假设账户 A 的余额为 1000 元。现在有两个事务事务 T1从账户 A 中扣除 100 元。事务 T2从账户 A 中扣除 200 元。如果两个事务都先读取余额再由应用程序根据读取结果计算新的余额可能出现以下情况。会话一执行START TRANSACTION; SELECT balance FROM account WHERE id 1;查询结果为 1000 元。与此同时会话二也读取同一条记录START TRANSACTION; SELECT balance FROM account WHERE id 1;查询结果同样为 1000 元。此时两个事务都基于旧余额进行计算。事务 T1 计算1000 - 100 900事务 T2 计算1000 - 200 800会话一执行UPDATE account SET balance 900 WHERE id 1; COMMIT;此时账户余额变为 900 元。会话二执行UPDATE account SET balance 800 WHERE id 1; COMMIT;如果该更新在等待会话一释放行锁后成功执行那么账户余额最终变为 800 元。但是正确的结果应该是1000 - 100 - 200 700最终结果却是 800 元说明 T1 的扣款结果被 T2 根据旧数据计算出的绝对值覆盖了。这就是丢失更新。需要注意上述过程假设两个事务通过应用程序读取旧值、计算绝对值并写回。实际的 SQL 执行会受到 InnoDB 行锁、事务隔离级别以及语句执行顺序等因素影响。对于上述场景我们可以直接在 SQL 中完成余额的增减而不是先查询余额再由应用程序计算并写入一个固定的新值。例如扣款操作可以写成UPDATE account SET balance balance - 100 WHERE id 1 AND balance 100;另一个事务扣款 200 元时可以执行UPDATE account SET balance balance - 200 WHERE id 1 AND balance 200;对于同一条记录InnoDB 会通过行锁协调并发更新。每条更新语句都会基于其实际执行时能够更新的当前记录值进行计算而不是简单地写入应用程序此前计算出的旧值。在前面的示例中如果两次扣款都成功那么最终余额就应该是 700 元。实际业务中还需要检查每条语句的受影响行数并将相关业务操作放入事务中。如果扣款失败或其他操作出现异常就应该根据业务要求回滚整个事务。处理丢失更新的重点是避免基于过期数据直接覆盖最新结果并通过合理的 SQL 更新方式与并发控制机制保护数据。四、事务的隔离级别在上一章的讲述中大家们可能会存在许多疑惑究竟是在什么场景下才会产生问题而又是如何解决上述问题的呢既然多个事务同时执行可能相互干扰产生问题那么 MySQL 应该如何控制它们之间的相互影响本章节将为你解答MySQL 的事务隔离机制。4.1 MySQL 的四种事务隔离级别MySQL 支持的四种事务隔离级别读未提交READ UNCOMMITTED隔离程度最低允许读取其他事务尚未提交的数据因此会出现脏读、不可重复读、幻读。读已提交READ COMMITTED只能读取已经提交的数据解决了脏读的问题但同一个事务的多次查询可能读到不同版本的数据出现不可重复读的问题。可重复读REPEATABLE READMySQL InnoDB 的默认隔离级别解决了不可重复读的问题即第一次读取就是最终版本当前事务不进行修改数据保证重复读取的一致性但 InnoDB 它也一并解决了幻读的问题而其他数据库不一定解决。串行化SERIALIZABLE隔离程度最高通过最严格的并发控制使事务的执行编写符合串行执行的要求好处是更安全代价是降低并发能力降低效率。隔离级别脏读不可重复读幻读读未提交 RU可能可能可能读已提交 RC不会可能可能可重复读 RR不会不会InnoDB 有专门处理机制串行化 Serializable不会不会不会4.2 读未提交READ UNCOMMITTED读未提交是四种隔离级别中限制最少的一种。在这种隔离级别下一个事务可以读取另一个事务尚未提交的修改因此可能产生脏读。为了验证这一点我们使用两个数据库会话进行实验。实验前提使用上一章创建的 accout 表确保账户 A 的 id 1初始余额为 1000 元。首先在会话一中开启事务并修改余额SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; START TRANSACTION; UPDATE account SET balance 500 WHERE id 1;注意此时不要提交事务。接着在会话二中执行SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; START TRANSACTION; SELECT balance FROM account WHERE id 1;此时会话二可能查询到以下结果balance500.00但是会话一对余额的修改还没有提交。接下来回到会话一执行ROLLBACK;会话一的修改被撤销账户余额恢复为 1000 元。这意味着会话二之前读取到的 500 元并不是一个已经提交的修改结果。这就是脏读。实验结束后在会话二执行COMMIT;通过实验可以发现读未提交允许事务读取其他事务尚未提交的数据因此可能发生脏读。这种隔离级别的并发限制较弱通常不适合对数据正确性要求较高的业务场景。4.3 读已提交READ COMMITTED读已提交要求事务只能读取已经提交的数据。与读未提交相比它能够避免脏读。但是它并不能保证同一个事务在多次查询时始终读取到相同的数据。在 InnoDB 中读已提交的普通一致性读通常会在每次查询时获取新的 Read View。因此当其他事务提交了数据修改后当前事务再次查询时就可能读取到新的数据版本。实验前提确保账户 A 的余额为 1000 元并且没有其他事务正在修改这条记录。首先在会话一中开启事务SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; START TRANSACTION; SELECT balance FROM account WHERE id 1;第一次查询结果为balance1000.00此时会话一不要提交事务。接着在会话二中修改账户余额并提交START TRANSACTION; UPDATE account SET balance 800 WHERE id 1; COMMIT;此时账户余额已经成功修改为 800 元。回到会话一再次执行相同的查询SELECT balance FROM account WHERE id 1;第二次查询结果为balance800.00可以发现会话一在同一个事务中两次执行相同的查询却得到了不同的结果。这就是不可重复读。实验结束后在会话一执行COMMIT;读已提交可以避免脏读但仍然可能出现不可重复读。其原因在于InnoDB 在 RC 隔离级别下普通一致性读通常为每次查询获取新的 Read View。当其他事务提交修改后后续查询就可能读取到新的数据版本。此外RC 也可能出现幻读。例如一个事务两次查询余额大于等于 600 元的账户在两次查询之间另一个事务插入了一条满足条件并提交的新记录那么第二次查询就可能多出一条记录。4.4 可重复读REPEATABLE READ可重复读是 InnoDB 默认的事务隔离级别。它的核心特点是在同一个事务中普通一致性读通常基于同一个 Read View 读取数据从而保证多次读取的一致性。也就是说即使其他事务修改并提交了数据当前事务重复执行普通查询时仍然可以读取到原来快照中的数据版本。实验前提确保账户 A 的余额为 1000 元并且没有其他事务正在修改这条记录。首先在会话一中开启事务并查询余额SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; START TRANSACTION; SELECT balance FROM account WHERE id 1;第一次查询结果为balance1000.00这次普通一致性读建立了当前事务使用的 Read View。接着在会话二中修改余额并提交START TRANSACTION; UPDATE account SET balance 800 WHERE id 1; COMMIT;此时账户的最新余额已经变为 800 元。但是会话一尚未结束事务。回到会话一再次执行SELECT balance FROM account WHERE id 1;第二次查询结果通常仍然为balance1000.00为什么不是 800 元因为在 RR 隔离级别下会话一的普通一致性读继续使用原来的 Read View。虽然会话二已经提交了修改但新版本不属于会话一当前快照可见的数据因此会话一仍然读取到 1000 元。实验结束后在会话一执行COMMIT;如果此时重新开启一个事务并查询余额就可以看到最新提交的 800 元。通过实验可以发现InnoDB 的 RR 能够避免普通一致性读中的脏读和不可重复读。不过这并不意味着 InnoDB 的所有查询都只读取快照中的历史版本。因此理解 RR 时需要区分快照读与当前读。MVCC 和锁机制会在后续章节中详细介绍。4.5 串行化SERIALIZABLE串行化是四种隔离级别中隔离程度最高的一种。它要求并发事务的执行效果符合串行执行的要求从而避免脏读、不可重复读和幻读等经典并发问题。为了实现更严格的隔离数据库可能需要让事务等待其他事务释放锁因此串行化通常会带来更高的并发控制开销。实验前提确保账户 A 的余额为 1000 元。首先在会话一中设置串行化隔离级别并开启事务SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; START TRANSACTION; SELECT balance FROM account WHERE id 1;查询结果为balance1000.00此时不要提交会话一的事务。在 InnoDB 中当使用显式事务且隔离级别为 SERIALIZABLE 时普通SELECT会采用共享锁定读的方式。接下来在会话二中执行START TRANSACTION; UPDATE account SET balance 800 WHERE id 1;此时会话二的UPDATE可能一直等待无法立即完成。这是因为会话一仍然持有相关记录的共享锁而会话二需要获取排他锁才能修改这条记录。现在回到会话一执行COMMIT;会话一提交事务并释放相关锁。此时会话二等待中的UPDATE就可以继续执行。待其成功完成后再在会话二执行COMMIT;最后查询账户余额可以看到修改后的结果为 800 元。通过实验可以发现在串行化隔离级别下数据库会采用更严格的并发控制方式必要时让事务等待从而限制并发事务对同一数据的访问。串行化能够提供更强的隔离但也可能增加锁等待和死锁的风险降低系统的并发能力。因此在实际项目中需要根据业务的一致性要求和并发压力选择合适的隔离级别。4.6 查看与设置事务隔离级别MySQL 允许我们查看和设置事务隔离级别。根据作用范围的不同可以分为全局级别GLOBAL和会话级别SESSION。4.6.1 查看事务隔离级别1. 查看全局事务隔离级别SELECT GLOBAL.transaction_isolation;该语句用于查看 MySQL 服务器当前的全局事务隔离级别也就是新建会话默认使用的隔离级别。2. 查看当前会话的事务隔离级别SELECT SESSION.transaction_isolation;该语句用于查看当前数据库连接的事务隔离级别。也可以简写为SELECT transaction_isolation;4.6.2 设置会话级别的事务隔离级别如果只想修改当前数据库连接的事务隔离级别可以使用SET SESSION。例如将当前会话的隔离级别设置为读已提交SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;设置完成后可以通过以下语句验证SELECT SESSION.transaction_isolation;需要注意会话级别的设置只对当前数据库连接生效不会影响其他连接也不会修改服务器的全局默认值。此外该设置不会改变已经开始执行的事务的隔离级别而是用于当前会话后续开启的事务。4.6.3 设置全局级别的事务隔离级别如果希望修改 MySQL 服务器的全局默认事务隔离级别可以使用SET GLOBAL。例如将全局隔离级别设置为读已提交SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;然后查看全局设置SELECT GLOBAL.transaction_isolation;此时全局默认隔离级别已经修改为READ-COMMITTED。需要注意以下几点只影响新建会话。修改全局隔离级别后新建立的数据库连接默认使用新的隔离级别。不影响已有会话。已经建立的数据库连接仍然保留各自的会话级别设置不会因为全局设置改变而自动更新。需要相应权限。修改全局设置需要具备相应的管理权限具体权限要求取决于 MySQL 版本和配置。重启后不一定保留。通过SET GLOBAL修改的设置通常只在当前服务器运行期间有效。如果希望重启后仍然生效需要通过配置文件或适用的持久化配置方式进行设置。