MySQL原理与实践(四):由数据库事务引出数据库隔离级别

(尊重劳动成果,转载请注明出处:https://yangwenqiang.blog.csdn.net/article/details/91049389冷血之心的博客)

关注微信公众号(文强的技术小屋),学习更多技术知识,一起遨游知识海洋~

快速导航:

 MySQL原理与实践(一):一条select语句引出Server层和存储引擎层

MySQL原理与实践(二):一条update语句引出MySQL日志系统

MySQL原理与实践(三):由三种数据结构引入MySQL索引及其特性

MySQL原理与实践(四):由数据库事务引出数据库隔离级别

MySQL原理与实践(五):数据库的锁机制

MySQL原理与实践(六):自增主键的使用

目录

前言:

正文:

事务:

事务的定义:

事务的四大特性:

事务的启动和提交方式:

事务并发操作产生的问题:

(1)脏读:

(2)不可重复读:

(3)幻读:

隔离级别:

事务隔离级别的实现:

回滚日志(undo log):

长事务的缺点:

事务启动时机对一致性视图的影响:

“快照”在 MVCC 里是怎么工作的?

事务的可重复读的能力是怎么实现的?

总结:


前言:

        对于看到这篇文章的小伙伴,我相信大家已经对MySQL是什么?MySQL的日志模块,MySQL的存储引擎以及其索引的特性和使用方法有了一个基本的认识。那么,接下来我们一起继续学习MySQL原理与实践(四)。在这篇文章中,我们主要介绍MySQL的事务,当数据库上有多个事务同时执行的时候,就可能出现 脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,我们接下来介绍隔离级别,并且重点介绍可重复读隔离级别的实现方法。本文整理总结于极客时间 -《MySQL实战45讲》,欢迎大家订阅学习,干货满满。

正文:

        我们先来介绍学习MySQL数据库的事务,接下来阐述事务可能出现的问题,然后阐述隔离级别机制。注意,并不是所有的MySQL存储引擎都支持事务操作,我们在文章MySQL原理与实践(一):一条select语句引出Server层和存储引擎层 介绍过MySQL的存储引擎MyIASM是不支持事务操作的。

事务:

事务的定义:

数据库事务,是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。简单来说,事务就是要保证一组数据库操作,要么全部成功,要么全部失败。

事务的四大特性:

          一般情况下,数据库的事务都满足四大特性,即(ACID):原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)

  • 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样
  • 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作
  • 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
  • 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失

事务的启动和提交方式:

       在 MySQL 命令行的默认设置下,事务都是自动提交的,即执行 SQL 语句后就会马上执行 COMMIT 操作。因此要显式地开启一个事务务须使用命令 begin 或start transaction。配套的提交语句是 commit,回滚语句是 rollback;或者执行命令 set autocommit=0,用来禁止使用当前会话的自动提交。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。

       设置了命令set autocommit=0 表示关闭自动提交事务,可能会存在问题。如果开启了一个事务却没有关闭,可能会导致接下来的查询都在该事务中,出现一个长事务。长事务的缺点我们后边再介绍。在MySQL数据库的 information_schema 库的 innodb_trx 这个表中查询长事务,比如下面这个语句,用于查找持续时间超过 60s 的事务。

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>

所以,大部分时候应该使用 set autocommit=1, 通过显式语句的方式来启动事务。

事务并发操作产生的问题:

        当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题。

(1)脏读:

    一个事务读到另一个事务还没有提交的记录。 如:

 

事务A读取了事务B未提交的数据,事务B的回滚,导致了事务A的数据不一致,导致了事务A的脏读 .

(2)不可重复读:

        一个事务在自己没有更新数据库数据的情况,同一个查询操作执行两次或多次的结果应该是一致的;如果不一致,就说明为不可重复读。还是用上面的例子:


这种情况事务A多次读取X的结果出现了不一致,即为不可重复读 。

(3)幻读:

       事务A读的时候读出了15条记录,事务B在事务A执行的过程中 增加 了1条,事务A再读的时候就变成了 16 条,这种情况就叫做幻影读。不可重复读说明了做数据库读操作的时候可能会出现的问题。

需要注意的是:

  • 不可重复读的重点是修改,同样的条件,你读取过的数据,再次读取出来发现值不一样了
  • 幻读的重点在于新增或者删除,同样的条件,第 1 次和第 2 次读出来的记录数不一样

为了解决多个事务并发操作数据库所产生得问题,提出了隔离级别得机制。

隔离级别:

为了避免数据库事务操作中的问题,在标准SQL规范中,定义了4个事务隔离级别,不同的隔离级别对事务的处理不同:

  • 读未提交(Read Uncommitted):允许脏读取,但不允许更新丢失。如果一个事务已经开始写数据,则另外一个数据则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现
  • 读已提交(Read Committed):允许不可重复读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行
  • 可重复读(Repeatable Read):禁止不可重复读取和脏读取,但是有时可能出现幻影数据。这可以通过“共享读锁”和“排他写锁”实现。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务
  • 序列化(Serializable):提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。

       隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed,它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、虚读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
      其中“读提交”和“可重复读”比较难理解,所以我用一个例子说明这几种隔离级别。假设数据表 T 中只有一列,其中一行的值为 1,下面是按照时间顺序执行两个事务的行为。

mysql> create table T(c int) engine=InnoDB;
insert into T(c) values(1);

我们来看看在不同的隔离级别下,事务 A 会有哪些不同的返回结果,也就是图里面 V1、V2、V3 的返回值分别是什么。

  • 若隔离级别是“读未提交”, 则 V1 的值就是 2。这时候事务 B 虽然还没有提交,但是结果已经被 A 看到了。因此,V2、V3 也都是 2。
  • 若隔离级别是“读提交”,则 V1 是 1,V2 的值是 2。事务 B 的更新在提交后才能被 A 看到。所以, V3 的值也是 2。
  • 若隔离级别是“可重复读”,则 V1、V2 是 1,V3 是 2。之所以 V2 还是 1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的。
  • 若隔离级别是“串行化”,则在事务 B 执行“将 1 改成 2”的时候,会被锁住。直到事务 A 提交后,事务 B 才可以继续执行。所以从 A 的角度看, V1、V2 值是 1,V3 的值是 2。

在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。

  • 在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。
  • 在“读提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。
  • 在“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念
  • 在“串行化”隔离级别下直接用加锁的方式来避免并行访问。

在不同的隔离级别下,数据库行为是有所不同的。Oracle 数据库的默认隔离级别其实就是“读提交”,而我们常用得数据库MySQL得默认隔离级别是“可重复读”。

事务隔离级别的实现:

      理解了事务的隔离级别,我们再来看看事务隔离具体是怎么实现的。这里我们展开说明“可重复读”。

回滚日志(undo log):

       在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。

  • 系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除
    • 什么时候才不需要了呢?
      • 就是当系统里没有比这个回滚日志更早的 read-view 的时候。

假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录。

       当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。如图中看到的,在视图 A、B、C 里面,这一个记录的值分别是 1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作得到。

同时你会发现,即使现在有另外一个事务正在将 4 改成 5,这个事务跟 read-view A、B、C 对应的事务是不会冲突的。

        回滚日志总不能一直保留吧,什么时候删除呢?答案是,在不需要的时候才删除。也就是说,系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除。

       什么时候才不需要了呢?就是当系统里没有比这个回滚日志更早的 read-view 的时候。

长事务的缺点:

长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。

       通过上面的介绍,可以看出,可重复读的隔离级别是借助于一致性视图实现的。但是事务的启动时机也会对一致性视图产生影响的。

事务启动时机对一致性视图的影响:

        begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才真正启动。如果你想要马上启动一个事务,可以使用 start transaction with consistent snapshot 这个命令。

  • 第一种启动方式,一致性视图是在第执行第一个快照读语句时创建的
  • 第二种启动方式,一致性视图是在执行 start transaction with consistent snapshot 时创建的。

接下来,我们介绍一致性视图在MVCC里的工作方式,以及当前读和快照读的概念。此处,我们先来建立一张表:

mysql> CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `k` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);

然后我们开启三个事务的执行流程:(限定当前的隔离级别是可重复读,autocommit=1)

        事务 C 没有显式地使用 begin/commit,表示这个 update 语句本身就是一个事务,语句完成的时候会自动提交。事务 B 在更新了行之后查询 ; 事务 A 在一个只读事务中查询,并且时间顺序上是在事务 B 的查询之后。

这里我们先来公布答案:A事务中查询结果是1,但是B事务中的查询结果是3.

我们接着来分析为什么是这个结果:

“快照”在 MVCC 里是怎么工作的?

        在可重复读隔离级别下,事务在启动的时候就“拍了个快照”。注意,这个快照是基于整库的。InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。

        每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。也就是说,数据表中的一行记录,其实可能有多个版本 (row),每个版本有自己的 row trx_id。

        图中虚线框里是同一行数据的 4 个版本,当前最新版本是 V4,k 的值是 22,它是被 transaction id 为 25 的事务更新的,因此它的 row trx_id 也是 25。

语句更新会生成 undo log(回滚日志)吗?那么,undo log 在哪呢?

答:图中的三个虚线箭头,就是 undo log;而 V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。比如,需要 V2 的时候,就是通过 V4 依次执行 U3、U2 算出来。

        按照可重复读的定义,一个事务启动的时候,能够看到所有已经提交的事务结果。但是之后,这个事务执行期间,其他事务的更新对它不可见。

因此,一个事务只需要在启动的时候声明说,“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”。

        当然,如果“上一个版本”也不可见,那就得继续往前找。还有,如果是这个事务自己更新的数据,它自己还是要认的。

        在实现上, InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。而数据版本的可见性规则,就是基于数据的 row trx_id 和这个一致性视图的对比结果得到的。这个视图数组把所有的 row trx_id 分成了几种不同的情况:

这样,对于当前事务的启动瞬间来说,一个数据版本的 row trx_id,有以下几种可能:

  • 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的

  • 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的

  • 如果落在黄色部分,那就包括两种情况

    • 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见

    • 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见

根据上边的介绍,我们再来看一下案例中三个事务的执行结果,再来贴一下上边的事务执行顺序图:

       我们先来分析一下事务A的执行结果吧,这个就比较easy了,根据我们上边介绍的一致性视图,事务B和事务C的启动都在事务A之后,也就是落在了高水位之后的未启动事务。所以,根据一致性读,事务A查询到的结果是1.

       再来分析事务B和事务C,首先是事务B开启了一个事务,并且创建了一个一致性视图。紧接着事务C直接执行了update更新语句,将(1,1)更新为(1,2).这里事务B的操作涉及到了快照读和当前读的问题了。

       事务B在事务C更新完毕之后,也接着进行了更新操作。那么此时究竟发生了啥?

        我们知道MySQL InnoDB引擎默认的修改数据语句,update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型。 也就是说事务B的这一条update更新语句,其实已经被加上了排他锁。有了排他锁,那么就不可以读取当前视图(快照)中的数据来更新了。也就是说当它要去更新数据的时候,就不能再在历史版本上更新了,否则事务 C 的更新就丢失了。因此,事务 B 此时的 set k=k+1 是在(1,2)的基础上进行的操作。也就是将数据从(1,2)更新为(1,3)。在执行事务 B 查询语句的时候,最新数据的版本号是自己的,也就是说这是自己的更新,可以直接使用,所以查询得到的 k 的值是 3。

当前读和快照读:

       快照读就是在当前的一致性视图(快照)里边进行读取数据,比如我们普通的select语句。而当前读就是将SQL语句加锁之后,为了保持数据的正确性,只读取当前(最新版本)的数据。例如update,delete和insert语句,以及select语句主动加上共享或者排他锁。

介绍了当前读和快照读之后,我们再来看看,如果将事务A的查询语句也进行了加锁操作,即:

mysql> select k from t where id=1 lock in share mode;
mysql> select k from t where id=1 for update;

那么这事务A的查询也从快照读变成了当前读,那么其执行结果也是当前的数据,那么其值也为3. 如果我们将事务C的事务启动和提交方式进行一个调整,如下所示:

       这种情况会发生什么呢? 其实这里涉及到了我们接下来会讲解的锁的问题。在update的时候,加了排他锁,当前事务没有提交的时候,其它事务是不可以在该行数据上继续 加锁的,也就是说事务B中的更新数据会被阻塞,知道事务C‘提交事务。

事务的可重复读的能力是怎么实现的?

        可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。

而读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:

  • 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
  • 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。

接下来,我们继续分析上边的三个事务,如果在隔离级别为读已提交的情况下,执行结果是多少?

这个结果比较easy了,事务A可以看到事务C中已经提交的更新,看不到事务B中未提交的更新,所以结果是2;事务B可以看到事务C和自己的更新,所以结果依旧是3.

总结:

这篇文章中,我们通过介绍事务引出了数据库的隔离级别,并且重点介绍了可重复读的内部实现原理。

        接下来,我们会介绍MySQL的加锁机制,并且在此基础上分析如何有效解决幻读这一问题。欢迎大家关注交流,希望对大家的学习有所帮助。

如果对你有帮助,记得点赞哦~欢迎大家关注我的博客,可以进群366533258一起交流学习哦~

本群给大家提供一个学习交流的平台,内设菜鸟Java管理员一枚、精通算法的金牌讲师一枚、Android管理员一枚、蓝牙BlueTooth管理员一枚、Web前端管理一枚以及C#管理一枚。欢迎大家进来交流技术。

关注微信公众号(文强的技术小屋),学习更多技术知识,一起遨游知识海洋~

发布了262 篇原创文章 · 获赞 2004 · 访问量 191万+

猜你喜欢

转载自blog.csdn.net/qq_25827845/article/details/91049389