V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
MySQL 5.5 Community Server
MySQL 5.6 Community Server
Percona Configuration Wizard
XtraBackup 搭建主从复制
Great Sites on MySQL
Percona
MySQL Performance Blog
Severalnines
推荐管理工具
Sequel Pro
phpMyAdmin
推荐书目
MySQL Cookbook
MySQL 相关项目
MariaDB
Drizzle
参考文档
http://mysql-python.sourceforge.net/MySQLdb.html
th3ee9ine
V2EX  ›  MySQL

解密 MySQL: MySQL 是如何确保数据完整性和一致性的?

  •  
  •   th3ee9ine · 316 天前 · 2656 次点击
    这是一个创建于 316 天前的主题,其中的信息可能已经有所发展或是发生改变。

    摘要:不管在什么场景下,数据的完整性和一致性都是至关重要的。MySQL 在这方面表现良好,通过多种机制,如:事务管理、ACID 、并发控制、锁机制等,全面保障数据的完整性和一致性,进而确保数据的可靠性和准确性。

    往期回顾:


    一、事务管理

    1 、什么是事务管理

    事务管理是数据库管理系统( DBMS )中的关键概念,用于确保数据库操作能够以一致、可靠的方式进行。

    事务是数据库中一组相关的操作,被视为一个不可分割的工作单元。这组操作要么全部执行成功,要么全部失败回滚,不存在部分执行的情况。

    事务管理的目标是保障数据库的完整性和一致性,确保在并发操作的环境下,多个事务能够正确地进行交互,而不会导致数据不一致或损坏。

    2 、事务管理与 ACID

    1 )什么是 ACID

    ACID 是数据库管理系统( DBMS )中用于描述事务特性的一组原则,其中包括:

    • 原子性( Atomicity ):事务是不可分割的工作单元,要么全部执行成功,要么全部失败回滚。
    • 一致性( Consistency ):事务的执行将数据库从一个一致性状态转变为另一个一致性状态。即:事务执行前后,数据库都必须保持一致性状态。
    • 隔离性( Isolation ):事务相互之间是独立的,一个事务的执行不应该影响其他事务的执行。
    • 持久性( Durability ):一旦事务提交,对数据库的修改将永久保存。即使在系统崩溃或重启后,数据库也应该能够恢复到事务提交后的状态。

    这些原则旨在确保在数据库执行的事务中维护数据的正确性和可靠性。

    2 )事务管理与 ACID 的关系

    • 事务管理是实现 ACID 的具体手段:通过事务的开始和结束、事务隔离级别、事务的提交和回滚等操作,事务管理确保了原子性、一致性、隔离性和持久性的实现。
    • 事务的边界:事务管理确定了事务的边界,即何时事务开始、何时提交,以及在何种情况下事务应该回滚。这些操作确保了事务在执行过程中遵循 ACID 的原则。
    • 事务隔离:事务管理通过设置事务隔离级别来控制事务之间的隔离程度,以确保隔离性的实现。不同的隔离级别影响事务在并发环境中的行为,从而影响了 ACID 中的隔离性。
    • 事务的恢复:持久性的实现涉及将事务的修改记录在事务日志中,并确保在事务提交后将这些修改持久保存到磁盘。事务管理通过事务日志等机制,实现了 ACID 中的持久性。

    事务管理是数据库系统中为了满足 ACID 属性而设计的一套机制和操作,通过这些机制,数据库系统可以确保事务的正确执行,维护数据的一致性和可靠性。ACID 属性是事务管理的核心原则,指导着事务在数据库中的正确执行和处理。

    #并发编程## 3 、事务的开始与结束

    在 MySQL 中,使用 START TRANSACTION 或者简单的 BEGIN 语句来标志事务的开始。

    事务的结束可以通过 COMMIT 来确认事务的全部操作成功,或者通过 ROLLBACK 来回滚事务,撤销事务的全部操作,将数据库恢复到事务开始前的状态。

    如下所示:

    -- 开始事务
    START TRANSACTION;  -- 或者使用 BEGIN;
    
    -- 提交事务
    COMMIT;
    
    -- 回滚事务
    ROLLBACK;
    

    1 )事务隔离级别

    MySQL 支持多个事务隔离级别,通过 SET TRANSACTION ISOLATION LEVEL 语句进行设置。不同的隔离级别影响事务在并发环境中的行为,包括读未提交、读已提交、可重复读和串行化。

    如下所示:

    -- 设置事务隔离级别
    SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
    

    2 )设置保存点

    MySQL 允许在事务执行期间设置保存点,以便在事务的不同阶段进行回滚,而不是回滚整个事务。这可以通过 SAVEPOINT 和 ROLLBACK TO 语句来实现。

    如下所示:

    -- 设置保存点
    SAVEPOINT savepoint_name;
    
    -- 回滚到保存点
    ROLLBACK TO savepoint_name;
    

    3 )事务嵌套

    MySQL 支持事务的嵌套执行,可以在一个事务中包含另一个事务。但需要注意,嵌套事务的使用要谨慎,确保能够正确提交或回滚各个层次的事务。

    -- 嵌套事务
    START TRANSACTION; -- 外层事务开始
    -- 在这里执行其他 SQL 语句
    SAVEPOINT inner_savepoint; -- 设置保存点
    -- 在这里执行其他 SQL 语句
    ROLLBACK TO inner_savepoint; -- 回滚到保存点
    COMMIT; -- 外层事务提交
    

    二、事务隔离级别

    1 、什么是事务隔离级别

    事务隔离级别是指在多个事务并发执行时,一个事务的执行不应该被其他事务干扰的程度。

    SQL 标准定义了四个隔离级别:

    • 读未提交:允许事务读取其他事务未提交的数据。
    • 读已提交:保证事务只能读取其他事务已经提交的数据。
    • 可重复读:在同一个事务中,多次读取同一数据得到相同的结果。
    • 串行化:最高级别的隔离,确保每个事务的执行效果对其他事务都是不可见的。

    从上面可以看出,隔离级别越高就是越接近串行化,而串行化就是按照事务的先后顺序执行,一个事务操作可能就要等待很久才能执行,并发执行的效率就没有了。

    不同隔离级别的用途就是为了提高并发执行效率,隔离级别越低,并发执行效率越高,资源的共享程度就越高。

    在都是读取操作的时候,资源共享有利于提高效率,并且不会造成问题。但是只要一个事务中出现写操作,就会带来问题。

    2 、并发事务可能造成的问题

    在并发事务下,可能会导致以下几种问题:

    问题 描述 例子
    脏读 一个事务读取了另一个事务未提交的数据。 事务 A 读取某一行数据,事务 B 修改了这行数据但尚未提交,事务 A 读取到了事务 B 未提交的修改。
    不可重复读 在同一个事务中,两次读取同一数据得到的结果可能不同,因为其他事务可能在两次读取之间修改了数据。 事务 A 两次读取同一行数据,事务 B 在两次读取之间修改了这行数据,导致事务 A 得到不同的结果。
    丢失更新 两个事务同时读取相同的数据,然后对其进行修改,最后只有一个事务的修改生效,另一个事务的修改被丢失。 事务 A 和事务 B 同时读取相同的数据,然后分别修改并提交,由于并发,只有一个事务的修改生效,另一个被覆盖。
    幻读 一个事务多次查询时,由于其他并发事务的插入或删除操作,导致看到了不同的数据行。这通常与范围查询有关。 事务 A 查询某一范围内的数据,事务 B 在该范围内插入了新的数据,导致事务 A 多次查询时看到了不同的数据行。

    从上文可以看出不可重复读与幻读其实在概念上很接近的,它们的主要区别在于:

    • 不可重复读:主要针对于某条数据在同一事务中前后读取不一致的问题。
    • 幻读:幻读是对于某一范围的数据集,发现查询数据集的行数多了或者少了,从而出现不一致的问题。

    3 、不同的事务隔离级别解决了什么问题

    对于上面的问题,不同的事务隔离级别都分别解决了什么问题呢?具体如下所示:

    问题 \ 隔离级别 读未提交 读已提交 可重复读 串行化
    脏读 无法解决 解决 解决 解决
    不可重复读 可能发生 解决 解决 解决
    丢失更新 无法解决 无法解决 解决 解决
    幻读 可能发生 可能发生 部分解决 解决

    从上面的表格中可以看出,可重复读隔离级无法完全解决幻读的问题,接下来我们就来具体了解一下:

    可重复读可以解决的场景:

    • 单行数据的读取:如果在一个事务中多次读取同一行数据,可重复读可以确保在事务的整个生命周期内读取的是一致的数据版本,从而避免不可重复读问题。
    • 范围查询中的数据读取:如果在一个事务中进行范围查询,可重复读可以确保在事务的整个生命周期内读取的是一致的数据版本,从而部分地避免幻读问题。

    可重复读无法解决的场景:

    • 范围查询中的数据插入: 如果在一个事务中进行范围查询,而另一个事务在查询范围内插入新的数据,可重复读无法阻止这种幻读情况的发生。新插入的数据将在事务的下一次查询中可见,导致幻读。
    • 范围查询中的数据删除: 如果在一个事务中进行范围查询,而另一个事务在查询范围内删除数据,可重复读同样无法阻止这种幻读情况的发生。被删除的数据将在事务的下一次查询中不再可见,也会导致幻读。

    总体而言,可重复读提供了一定程度的幻读保护,但无法完全消除所有可能的幻读情况。如果对于幻读问题有更严格的要求,可以考虑使用更高的隔离级别,如串行化,但需要注意可能带来的性能开销。

    4 、四种隔离级别具体是如何实现的

    • 读未提交:因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了。
    • 读已提交:通过 Read View 来实现的,读已提交是在每个语句执行前都会重新生成一个 Read View ,
    • 可重复读:通过 Read View 来实现的,可重复读会在启动事务时生成一个 Read View ,然后整个事务期间都在用这个 Read View 。
    • 串行化:通过加读写锁的方式来避免并行访问。

    注:

    • 读已提交和可重复读,它们都是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同。
    • 读已提交隔离级别是在每个 select 都会生成一个新的 Read View ,也意味着,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。
    • 可重复读隔离级别是启动事务时生成一个 Read View ,然后整个事务期间都在用这个 Read View ,这样就保证了在事务期间读到的数据都是事务启动前的记录。

    在可重复读隔离级别中,普通的 select 语句就是基于 MVCC 实现的快照读,也就是不会加锁的。而 select .. for update 语句就不是快照读了,而是当前读了,也就是每次读都是拿到最新版本的数据,但是它会对读到的记录加上 next-key lock 锁。

    5 、Read View 是什么

    那什么又是 Read View 呢?

    Read View 是一个数据库的内部快照,该快照被用于 InnoDB 存储引擎中的 MVCC 机制,它保存着数据库某个时刻的数据信息。

    Read View 会根据事务的隔离级别决定在某个事务开始时,该事务能看到什么信息。

    快照.jpeg

    注意,执行开始事务命令,并不等于启动了事务。在 MySQL 有两种开启事务的命令,分别是:

    • begin/start transaction
    • start transaction with consistent snapshot

    这两种开启事务的命令,事务的启动时机是不同的:

    • 执行了 begin/start transaction 命令后,并不意味着事务启动了。只有在执行这个命令后,执行了增删查改操作的操作,才是事务真正启动的时机。
    • 执行了 start transaction with consistent snapshot 命令,就会马上启动事务。

    上面提到了 MVCC 机制,接下来我们来具体了解一下什么是 MVCC 机制,以及 Read View 在 MVCC 里是如何工作的。

    三、MVCC (多版本并发控制)

    1 、什么是 MVCC

    MVCC ( Multi-Version Concurrency Control )是一种数据库管理系统中用于处理并发事务的机制。它的原理是通过为每个事务创建数据的多个版本,以允许多个事务同时并发执行而不会相互干扰。

    MVCC 的作用是确保事务的隔离性,防止并发事务中的读取和写入冲突,从而维护数据库的一致性。

    MVCC 的基本原理包括以下几个关键点:

    • 版本标识:每行数据都带有一个版本标识,通常是一个版本号或时间戳,表示数据的创建时间或最后修改时间。
    • 事务开始标识:每个事务在开始时都有一个标识,通常是全局唯一的事务 ID 或时间戳。
    • 数据版本的创建:当事务对数据库进行写操作时,不是直接在原有数据上进行修改,而是为修改后的数据创建一个新版本。这个新版本的版本标识与当前事务的开始标识相关联。
    • 事务的读取操作:当事务执行读取操作时,系统根据事务开始标识,选择合适的数据版本返回给该事务。事务只能看到在其开始时已经存在的数据版本,不会看到在其开始后被其他事务修改的数据。
    • 并发事务的处理:多个事务可以并发执行,因为它们使用不同的事务开始标识和数据版本。这避免了读取到未提交数据、数据不一致等问题。
    • 历史数据的保留:MVCC 使系统能够保留历史数据版本,而不是简单地覆盖旧版本。这为实现一些特殊功能,如快照隔离、历史数据查询、回滚等,提供了便利。

    MVCC 的作用:

    • 并发控制:MVCC 解决了并发事务中的读取和写入冲突,确保事务的隔离性,防止不一致的数据被并发事务读取和修改。
    • 快照隔离:MVCC 支持快照隔离级别,使得每个事务都能够看到在其开始时存在的一致性数据快照,而不受其他事务的影响。

    2 、Read View 在 MVCC 里是如何工作的

    1 )什么是 Read View

    在 MVCC 中,每行数据都有一个版本号或时间戳,表示该行数据的创建时间或最后修改时间。每个事务在开始时会创建一个 "Read View"(读视图),该视图定义了该事务所能看到的数据版本范围。

    Read View 是事务用于确定其可见数据版本范围的一种重要数据结构。Read View 记录了事务开始时的全局事务标识,以及其他用于决定可见数据范围的信息。

    2 ) Read View 的构成

    Read View 的组成结构大致如下图所示:

    ReadView.png

    各个字段含义,如下所示:

    • creator_trx_id (创建者事务 ID ):表示创建该 Read View 的事务的 ID 。这个 ID 是唯一标识一个事务的数字,用于确定在创建 Read View 的时刻,哪个事务创建了这个快照。
    • m_ids (活跃事务 ID 列表):这是一个列表,包含在创建 Read View 时数据库中所有 活跃事务 的 ID 。活跃事务 是指已经启动但尚未提交的事务。Read View 需要跟踪这些活跃事务,以确保读取数据时可以考虑它们的未提交修改。
    • min_trx_id (最小事务 ID ):表示在创建 Read View 时,数据库中所有活跃事务中最小的事务 ID 。它是 m_ids 列表中的最小值。这个值用于确定 Read View 的创建时刻,在这之前已经完成的事务。
    • max_trx_id (最大事务 ID ):这个值并不是 m_ids 列表的最大值,而是表示创建 Read View 时,数据库应该给下一个事务分配的 ID 值。实际上,它是全局事务中最大的事务 ID 值加 1 。这确保新事务的 ID 不会与已经存在的事务 ID 重叠。

    总的来说,Read View 通过记录创建者事务 ID 、活跃事务 ID 列表、最小事务 ID 和最大事务 ID ,提供了一个一致性的快照视图,以支持多版本并发控制。

    这有助于确保事务在读取数据时能够看到一致性的状态,同时处理活跃事务的未提交修改。

    3 )举个例子

    接下我们就通过下面这个例子,来进一步了解一下这些字段是如何被使用的。

    假设我们有一张账户余额表,小明在其中有中一条余额为 50 万的记录,如下图所示:

    ![隐藏列.png](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)

    其中 trx_id 、roll_pointer 为隐藏列,对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含这两个隐藏列。

    • trx_id (事务 ID ):该隐藏列用于标识最后一次对聚簇索引记录进行修改的事务 ID 。每当对记录进行更新时,新的版本将包含新的 trx_id 。这样,可以跟踪事务对记录的修改历史。
    • roll_pointer (回滚指针):每次对聚簇索引记录进行修改时,InnoDB 会将旧版本的记录写入 undo 日志中,以提供回滚操作的支持。roll_pointer 是一个指针,指向 undo 日志中存储的旧版本记录,这样可以通过它找到修改前的记录。

    这两个隐藏列的存在使得 InnoDB 能够实现多版本并发控制( MVCC ),从而提供了事务的隔离性和一致性。通过这些信息,InnoDB 可以追踪记录的修改历史,支持事务的回滚操作,以及在并发读取时提供一致的快照视图。这对于数据库引擎的性能和并发控制来说是至关重要的。

    在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况,如下图所示:

    事务 id.png

    • 第一种情况:如果记录的 trx_id 的值小于 min_trx_id 的值,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。
    • 第二种情况:如果记录的 trx_id 的值大于等于 max_trx_id 的值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。
    • 第三种情况:如果记录的 trx_id 的值在 min_trx_id 的值和 max_trx_id 的值之间,则需要判断 trx_id 是否在 m_ids 列表中:
      • 如果记录的 trx_id 在 m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。
      • 如果记录的 trx_id 不在 m_ids 列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。

    这就是 MVCC 的核心原理,这样确保了 Read View 对事务的隔离性和一致性。活跃事务(未提交的事务)对于其他事务是不可见的,已提交事务则是可见的。

    3 、MVCC 的应用场景

    1 )读已提交

    读已提交是在每次读取数据时,都会生成一个新的 Read View 。

    也就是说,在事务执行期间,多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能在这期间另外一个事务修改了该记录,并提交了事务。

    我们以上面小明的例子来举例:

    假设现在有两个事务,事务 A (事务 id 为 11 )启动后,紧接着事务 B (事务 id 为 12 )也启动了,两个事务的运行顺序如下所示:

    • 事务 A 读取数据(创建 Read View ),小明的账户余额为 50 万。
    • 事务 B 读取数据(创建 Read View ),小明的账户余额为 50 万。
    • 事务 A 修改小明的账户余额(还没提交事务),将小明的账户余额从 50 万修改成了 100 万。
    • 事务 B 读取数据(创建 Read View ),小明的账户余额为 50 万。
    • 事务 A 提交事务。
    • 事务 B 读取数据(创建 Read View ),小明的账户余额为 100 万。

    前两次事务 B 读取数据时创建的 Read View 如下图所示:

    事务 B 第一次读取数据:

    ![事务 B 创建的 ReadView](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)

    事务 A 修改数据:

    事务 A 修改数据

    事务 B 第二次读取数据:

    事务 B 第二次创建的 ReadView

    从上面的 3 张图我们可以知道:

    • 第一步:事务 B 在找到小明的这条记录时,会对比这条记录的 trx_id (值为 11 ),发现 trx_id 在事务 B 的 Read View 的 min_trx_id 和 max_trx_id 之间。
    • 第二步:接下来会判断 trx_id 值是否在 m_ids 范围内,判断的结果为:是。那么说明这条记录是被还未提交的事务修改的,这时事务 B 并不会读取这个版本的记录。
    • 第三步:根据 undo log 查找找旧版本的记录,直到找到 trx_id 小于事务 B 的 Read View 中的 min_trx_id 值的第一条记录,所以事务 B 能读取到的是 trx_id 为 10 的记录,也就是小明余额是 50 万的这条记录。

    那为什么事务 A 提交后,事务 B 就可以读到事务 A 修改的数据?

    因为在事务 A 提交后,由于隔离级别是读提交,所以事务 B 在每次读数据的时候,会重新创建 Read View ,此时事务 B 第三次读取数据时创建的 Read View 如下:

    ![事务 B 第三次创建的 ReadView](data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='1px' height='1px' viewBox='0 0 1 1' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3E%3C/title%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' fill-opacity='0'%3E%3Cg transform='translate(-249.000000, -126.000000)' fill='%23FFFFFF'%3E%3Crect x='249' y='126' width='1' height='1'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)

    事务 A 修改数据

    从上图中我们可以知道,事务 B 在找到小明的这条记录时,会对比这条记录的 trx_id (值为 11 ),发现比事务 B 的 Read View 中的 min_trx_id (值为 12 )小,这意味着修改这条记录的事务早就在创建 Read View 前提交过了,所以该版本的记录对事务 B 是可见的。

    通过上面的例子,我们知道了在读提交隔离级别下,事务每次读数据时都重新创建 Read View ,那么在事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务。

    接下来我们就继续了解一下可重复读是如何避免这个问题的。

    2 )可重复读

    可重复读是启动事务时生成一个 Read View ,然后整个事务期间都在用这个 Read View 。

    我们继续以上面小明的例子来举例:

    假设现在有两个事务,事务 A (事务 id 为 11 )启动后,紧接着事务 B (事务 id 为 12 )也启动了,两个事务的运行顺序如下所示:

    • 事务 A 读取数据(创建 Read View ),小明的账户余额为 50 万。
    • 事务 B 读取数据(创建 Read View ),小明的账户余额为 50 万。
    • 事务 A 修改小明的账户余额(还没提交事务),将小明的账户余额从 50 万修改成了 100 万。
    • 事务 B 读取数据(继续使用上面创建的 Read View ),小明的账户余额为 50 万。
    • 事务 A 提交事务。
    • 事务 B 读取数据(继续使用上面创建的 Read View ),小明的账户余额仍然为 50 万。

    事务 A 和 事务 B 的 Read View 具体如下图所示:

    事务 A 创建的 ReadView

    事务 B 创建的 ReadView

    小明余额原始记录

    从上图我们可以知道:

    事务 B 第一次读小明的账户余额记录,在找到记录后,它会先看这条记录的 trx_id ,发现 trx_id (值为 10 )比事务 B 的 Read View 中的 min_trx_id (值为 11 )还小,这意味着修改这条记录的事务早就在事务 B 启动前提交过了,所以该版本的记录对事务 B 可见的。

    事务 A ,将小明的余额修改为 100 万,但未提交事务,这时 MySQL 会记录相应的 undo log ,并以链表的方式串联起来,如下图所示:

    事务 A 修改数据

    因为事务 A 修改了记录,之前的记录就变成旧版本记录了,最新的 trx_id 是事务 A 的事务 id (值为 11 )。

    事务 B 第二次去读取该记录,会去对比这条记录的 trx_id (值为 11 ),发现在 min_trx_id 和 max_trx_id 之间,且 trx_id 值是在 m_ids 范围内的,那么说明这条记录是被还未提交的事务修改的,这时事务 B 并不会读取这个版本的记录。而是沿着 undo log 链条往下找,直到找到 trx_id 小于事务 B 的 min_trx_id 值的第一条记录,所以事务 B 读取到的是 trx_id 为 10 的记录,也就是小明余额是 50 万的这条记录。

    最后,当事务 A 提交事务后,由于隔离级别为可重复读,所以事务 B 再次读取记录时,还是基于启动事务时创建的 Read View 来判断当前版本的记录是否可见。

    所以,即使事务 A 将小明余额修改为 100 万并提交了事务,事务 B 第三次读取记录时,读到的记录仍然是小明的余额是 50 万的这条记录。

    通过上面两个例子,我们清楚地了解了 MySQL 是如何实现的读已提交和可重复读隔离级别。虽然可重复读隔离级别在很大程度上解决了幻读问题,但并不能完全消除幻读的问题。在特定情况下,依然可能出现由于插入或删除操作引起的幻读现象。

    4 、如何完全解决幻读问题?

    针对不同场景,MySQL 采用了两种不同的解决方案,具体如下:

    • 快照读:
      • 只需要通过 MVCC 的方式就可以解决幻读问题。在可重复读隔离级别下,事务在执行期间看到的数据快照始终与事务启动时看到的数据快照一致。即使在事务执行期间有其他事务插入了新的数据,这些新插入的数据对当前事务来说是不可见的。
      • 快照读提供了一致的数据快照,对于并发访问来说,可以避免读到未提交的新数据,从而避免了幻读问题。
    • 当前读:
      • 通过 next-key lock (记录锁和间隙锁)的方式解决了幻读问题。当执行 select ... for update 这种当前读的语句的时候,如果有其他事务在 next-key lock 锁范围内插入了一条记录,插入语句将会被阻塞,无法成功插入,确保当前事务能够读到稳定的数据快照,避免了幻读问题。
      • 当前读通过锁的方式保证了在事务期间读到的数据是最新的,但也引入了锁的开销。

    额外知识点:

    • 快照读( Snapshot Read ):

      • 快照读是指在事务开始时创建一个事务快照,并在整个事务期间使用这个快照来读取数据。在快照读中,事务看到的数据是在事务启动时刻的一个一致的快照。
      • 在可重复读隔离级别下的普通 SELECT 语句就是一种快照读。
    • 当前读( Current Read ):

      • 当前读是指在事务执行期间,每次读取都会获取最新的数据版本。事务执行期间,如果其他事务对数据进行了修改并提交,当前读会读取到最新的已提交版本的数据。
      • 当前读保证了读取到最新的数据,但可能受到其他事务修改的干扰,因为在读取数据的过程中,数据可能会被其他事务修改。
      • SELECT ... FOR UPDATE 和 SELECT ... FOR SHARE 等语句是当前读。

    四、结尾

    通过上面的内容,我们了解了 MySQL 是如何通过事务隔离级别、MVCC 等机制来确保数据的一致性。其中还涉及到了 MySQL 的锁机制、事务日志等,由于篇幅限制,这些方面的细节暂未详细展开。在后续的文章中,我们将继续深入探讨这些内容。

    总体而言,MySQL 在保障数据完整性和一致性方面的设计和实现,为开发者提供了强大的能力,让他们能够构建稳定、可靠的应用程序。通过合理选择隔离级别、优化查询语句以及合理设计数据库结构,开发者可以更好地发挥 MySQL 的能力,确保数据在复杂的多用户并发操作中能够保持一致性,从而为用户提供更好的使用体验。

    参考资料

    • 高性能 MySQL(第三版)
    • MySQL 官方手册
    • 图解 Mysql

    原文链接:解密 MySQL:MySQL 是如何确保数据完整性和一致性的?

    微信公众号:啊杰在拱趴

    求关注!!!!

    第 1 条附言  ·  315 天前

    图片补充: 隐藏列: 隐藏列.png

    事务 B 第一次读取数据: 事务B创建的ReadView

    事务 B 第三次创建的 ReadView: 事务B第三次创建的ReadView

    22 条回复    2024-01-18 17:47:35 +08:00
    littlewing
        1
    littlewing  
       316 天前
    提个建议哈,那些图不要截图,直接导出 jpg png 之类的
    victoriamerrick
        2
    victoriamerrick  
       316 天前
    楼主是福州人吗
    kuituosi
        3
    kuituosi  
       316 天前
    好笨啊,自己不思考网上随便抄的文章,但是你不觉得标题怪怪的吗?
    既然 mysql 保证了一致性为什么又提供了隔离级别这种东西?
    隔离级别的作用到底是干什么的?
    taogen
        4
    taogen  
       316 天前
    先翻在看。翻到最后,不出意外,果然有公众号
    xwayway
        5
    xwayway  
       316 天前 via iPhone
    卷到这里来了吗
    Betsy
        6
    Betsy  
       315 天前 via iPhone
    整理的蛮详细哈,好奇 OP 有没有自己实现过这套机制?
    jefferyJQ
        7
    jefferyJQ  
       315 天前
    这样吗?
    th3ee9ine
        8
    th3ee9ine  
    OP
       315 天前
    @kuituosi 你看完了整篇文章了不?
    th3ee9ine
        9
    th3ee9ine  
    OP
       315 天前
    @littlewing 好的,下次我注意一下
    th3ee9ine
        10
    th3ee9ine  
    OP
       315 天前
    th3ee9ine
        11
    th3ee9ine  
    OP
       315 天前
    @taogen 你可以只在这里看,也是可以的。
    th3ee9ine
        12
    th3ee9ine  
    OP
       315 天前
    @Betsy MVCC 有在开发场景使用过
    th3ee9ine
        13
    th3ee9ine  
    OP
       315 天前
    @jefferyJQ roll_pointer 的值写的不对哦,上一条应该指向下一条
    JackSlowFcck
        14
    JackSlowFcck  
       315 天前
    @taogen 所以,直接看评论,啊哈哈哈哈
    th3ee9ine
        15
    th3ee9ine  
    OP
       315 天前
    @xwayway 总结一下知识点,也算卷吗?在公司不干活,只冲时常的那不是得卷坏了。
    kuituosi
        16
    kuituosi  
       315 天前
    @th3ee9ine 不用看完啊,抄袭加拼凑没有多少价值
    水平太差都不够看的
    th3ee9ine
        17
    th3ee9ine  
    OP
       315 天前
    @kuituosi 看了你的主页,一个技术方案都要发几篇文章抄的人,你的技术确实高,确实不够您看的。
    kuituosi
        18
    kuituosi  
       315 天前
    @th3ee9ine 看我的主页干嘛?讲 mysql 的一致性却讲不明白隔离级别是干啥用的?
    抄了这么多文字自己都没有搞明白的东西,这种水平还敢出来碰瓷别人的技术
    th3ee9ine
        19
    th3ee9ine  
    OP
       315 天前
    @kuituosi 要么你眼睛不好使,要么是你脑子不好使,你的技术我确实不敢碰瓷,太强了。
    kuituosi
        20
    kuituosi  
       315 天前
    @th3ee9ine 着急了开始人身攻击了
    jefferyJQ
        21
    jefferyJQ  
       315 天前
    @th3ee9ine #13 改动前面的快照?不合理吧?
    th3ee9ine
        22
    th3ee9ine  
    OP
       315 天前   ❤️ 1
    @jefferyJQ 我重新看了一下,是我看错了,早上我指针方向看反了。最新的记录指向上一条旧的记录,你画的是对的。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1082 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 20:15 · PVG 04:15 · LAX 12:15 · JFK 15:15
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.