云计算百科
云计算领域专业知识百科平台

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

目录

本文内容概要

一、认识事务

1.1 什么是事务

1.2 为什么需要事务

1.3 事务的四大特性(ACID)

1.3.1 原子性(Atomicity)

1.3.2 一致性(Consistency)

1.3.3 隔离性(Isolation)

1.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 UNCOMMITTED)

4.3 读已提交(READ COMMITTED)

4.4 可重复读(REPEATABLE READ)

4.5 串行化(SERIALIZABLE)

4.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 元。

    转账前,账户状态如下:

    用户账户余额
    A 1000 元
    B 500 元
    总余额 1500 元

    正常情况下,转账完成之后,账户状态应该变为:

    用户账户余额
    A 900 元
    B 600 元
    总余额 1500 元

    可以看到,转账只是改变了资金在两个账户之间的分配,并没有改变两个账户的总余额。

    但是,如果没有事务的管理,假设 A 的扣款操作已经成功,而 B 的入账操作因为某种原因失败,那么数据库中的状态就会变成:

    用户账户余额
    A 900 元
    B 500 元
    总余额 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;

    结果如下:

    idnamebalance
    1 A 1000.00
    2 B 500.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;

    结果如下:

    idnamebalance
    1 A 900.00
    2 B 600.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;

    如果这期间没有其他事务修改账户数据,那么查询结果仍然是:

    idnamebalance
    1 A 900.00
    2 B 600.00

    这是因为本次事务的扣款操作并没有提交,而是通过 ROLLBACK 撤销了。

    三、并发事务带来的问题

    在上一章中,我们详细介绍了 MySQL 事务的基本操作,包括事物的开启、提交、回滚以及保存点。但是,在实际业务中,往往会有多个用户同时访问数据库。如果多个事务同时执行,那么它们之间可能相互影响,产生并发问题。

    3.1 脏读

    脏读是指一个事务读取到了另一个事务尚未提交的数据。

    了解:为什么被称为 “脏” 读呢?

    因为另一个事务尚未提交,其修改结果最终可能被回滚。如果当前事务读取了这些数据,那么它读取到的就是一份永远不会正式生效的数据。

    下面通过两个并发事务进行演示:

    假设账户 A 的初始余额为 1000 元

    事务 T1:

    START 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;

    此时,会话二可能查询到:

    balance
    500.00

    但是,账户 A 的余额修改尚未提交。

    接下来,会话一执行:

    ROLLBACK;

    由于事务 T1 回滚了,账户 A 的余额又恢复为 1000 元。

    这意味着,会话二之前读取到的 500 元,实际上并没有成为最终生效的数据。这就是脏读。

    3.2 不可重复读

    不可重复读是指同一个事务在前后两次读取同一条记录时,得到了不同的结果。

    需要注意的是,这里强调的是同一个事务对同一条记录进行多次读取。

    与脏读不同,不可重复读是因为另一个事务修改了相关数据,并且提交了修改结果。

    假设账户 A 的初始余额为 1000 元。

    为了方便观察,我们将会话一设置为读已提交隔离级别。

    事务 T1:

    SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

    START TRANSACTION;

    SELECT balance
    FROM account
    WHERE id = 1;

    第一次查询结果为:

    balance
    1000.00

    此时,事务 T1 暂时不提交。

    事务 T2:

    UPDATE account
    SET balance = 800
    WHERE id = 1;

    在默认自动提交开启的情况下,这条语句成功执行后,其修改会自动提交。

    此时,数据库中的账户余额已经变为 800 元。

    接下来,会话一再次执行相同的查询:

    SELECT balance
    FROM account
    WHERE id = 1;

    第二次查询结果为:

    balance
    800.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 READ):MySQL 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;

    此时,会话二可能查询到以下结果:

    balance
    500.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;

    第一次查询结果为:

    balance
    1000.00

    此时,会话一不要提交事务。

    接着,在会话二中修改账户余额并提交:

    START TRANSACTION;

    UPDATE account
    SET balance = 800
    WHERE id = 1;

    COMMIT;

    此时,账户余额已经成功修改为 800 元。

    回到会话一,再次执行相同的查询:

    SELECT balance
    FROM account
    WHERE id = 1;

    第二次查询结果为:

    balance
    800.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;

    第一次查询结果为:

    balance
    1000.00

    这次普通一致性读建立了当前事务使用的 Read View。

    接着,在会话二中修改余额并提交:

    START TRANSACTION;

    UPDATE account
    SET balance = 800
    WHERE id = 1;

    COMMIT;

    此时,账户的最新余额已经变为 800 元。

    但是,会话一尚未结束事务。

    回到会话一,再次执行:

    SELECT balance
    FROM account
    WHERE id = 1;

    第二次查询结果通常仍然为:

    balance
    1000.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;

    查询结果为:

    balance
    1000.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 修改的设置通常只在当前服务器运行期间有效。如果希望重启后仍然生效,需要通过配置文件或适用的持久化配置方式进行设置。

  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » MySQL 基础篇(十一):事务与隔离级别
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!