mysql原理3-事务

2021/01/06 Mysql 共 13075 字,约 38 分钟

基础

用到的文件存储位置:

buffer pool -> page cache -> 磁盘。

  • page cache是系统的缓存,通过fsync才能落到磁盘,再落到磁盘之前宕机都可能会丢失。操作系统缓存不会轻易丢失。 操作系统会根据自己调度策略(如缓存满了或超时)将数据刷新到磁盘。
  • buffer pool是利用内存自己实现的缓存。

必背术语:

  • 加锁读也叫当前读。 行写锁,表锁,Update, Insert都是当前读。
  • 非加锁读也叫快照读

并发问题

  1. 脏读 :脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。

  2. 不可重复读 :是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。

  3. 幻读 : 是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。

脏读、不可重复读和幻读

首先来看并发情况下,读操作可能存在的三类问题:

(1)脏读:当前事务(A)中可以读到其他事务(B)未提交的数据(脏数据),这种现象是脏读。举例如下(以账户余额表为例):

img

(2)不可重复读:在事务A中先后两次读取同一个数据,两次读取的结果不一样,这种现象称为不可重复读。脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。举例如下:

img

(3)幻读:在事务A中按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了。举例如下:

img

事务特性: ACID

ACID是衡量事务的四个特性:

  • 原子性(Atomicity):事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。

    • 回滚可以用回滚日志(Undo Log)来实现,回滚日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。
  • 一致性(Consistency):数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对同一个数据的读取结果都是相同的。

    • 如何理解一致性?应用系统从一个正确的状态到另一个正确的状态

    • 数据库一致性,是数据库中的数据开始是正确的,随着状态转移,总是保持正确的状态。用户在任意时刻的任何请求返回的都是正确的结果。数据库以一定的模式存储数据,本质是对真实世界建模,因此这里的正确是指,数据满足真实世界各种约束(例如:完整性约束)。

      事务一致性,事务开始前数据库处于一致状态,结束后数据库依旧满足一致性。

      数据库状态转移的基本单位是事务,为了保证数据正确,状态转移之间不应相互干扰(应依次执行),否则数据会出现不确定的结果(想象一下1000个线程没有保护的同时修改一个数据)。这也是就是很多资料里将一致性描述为:“事务开始和结束之间的中间状态不会被其他事务看到”的原因了。

  • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。一个事务所做的修改在最终提交以前,对其它事务是不可见的。

    • 与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响。隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。

      隔离性追求的是并发情形下事务之间互不干扰。简单起见,我们主要考虑最简单的读操作和写操作(加锁读等特殊读操作会特殊说明),那么隔离性的探讨,主要可以分为两个方面:

      • (一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性
      • (一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性
  • 持久性(Durability):一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。

    系统发生崩溃可以用重做日志(Redo Log)进行恢复,从而实现持久性。与回滚日志记录数据的逻辑修改不同,重做日志记录的是数据页的物理修改。

这几个特性不是一种平级关系:

  • 只有满足一致性,事务的执行结果才是正确的。
  • 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。
  • 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
  • 事务满足持久化是为了能应对系统崩溃的情况。

按照严格的标准,只有同时满足ACID特性才是事务;但是在各大数据库厂商的实现中,真正满足ACID的事务少之又少。例如MySQL的InnoDB默认事务隔离级别是可重复读,不满足隔离性;Oracle默认的事务隔离级别为READ COMMITTED,不满足隔离性……因此与其说ACID是事务必须满足的条件,不如说它们是衡量事务的四个维度。

基本数据库的默认方式都不满足隔离性和一致性,除非可串行化。

事务隔离级别

SQL标准中定义了四种隔离级别,并规定了每种隔离级别下上述几个问题是否存在。一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差。隔离级别与读问题的关系如下:

img

在实际应用中,读未提交在并发时会导致很多问题,而性能相对于其他隔离级别提高却很有限,因此使用较少。可串行化强制事务串行,并发效率很低,只有当对数据一致性要求极高且可以接受没有并发时使用,因此使用也较少。因此在大多数数据库系统中,默认的隔离级别是读已提交(如Oracle)或可重复读(后文简称RR)。Mysql的是可重复读。

可以通过如下两个命令分别查看全局隔离级别和本次会话的隔离级别:

img

img

InnoDB默认的隔离级别是RR,后文会重点介绍RR。需要注意的是,在SQL标准中,RR是无法避免幻读问题的,但是InnoDB实现的RR用了MVCC避免了幻读问题。

原子性及实现:undo log

1. 定义

原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。

2. 实现原理:undo log

原子性是事务的基本特性,保证了事务中的操作是不可拆分的整体,那么原子性是如何实现的呢?事务的原子性表现的两个方面:

  • 事务提交成功时,那么事务中的操作总会完成
  • 事务提交成功保证事务中的操作都会完成。1、是正确执行完事务,没有出现任何问题;2、是事务提交成功但是出异常,数据库恢复之后,提交完成的事务会保证数据库完成该事物的操作。对于第一种正常情况不予讨论,因为不存在 异常情况,那么第2种实际上是和上文说的持久性是相关联的,而这个是基于重做日志(redo log)来保证提交完成的事务在异常情况下保证数据操作能够进行:
  • 事务提交失败,那么事务中的操作都失败
  • 事务提交失败,那么事务中的操作都失败,这个是通过数据库的撤销操作日志来保证的,也称之为undo log。

因此可以说是实现原理可以是undo log , 就是说不存在中间状态。

在说明原子性原理之前,首先介绍一下MySQL的事务日志。MySQL的日志有很多种,如二进制日志、错误日志、查询日志、慢查询日志等,此外InnoDB存储引擎还提供了两种事务日志:redo log(重做日志)和undo log(回滚日志)。其中redo log用于保证事务持久性;undo log则是事务原子性和隔离性实现的基础。

下面说回undo log。实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的sql语句。InnoDB实现回滚,靠的是undo log:当事务对数据库进行修改时,InnoDB会生成对应的undo log;如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。

undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo log的内容做与之前相反的工作:对于每个insert,回滚时会执行delete;对于每个delete,回滚时会执行insert;对于每个update,回滚时会执行一个相反的update,把数据改回去。

以update操作为例:当事务执行update时,其生成的undo log中会包含被修改行的主键(以便知道修改了哪些行)、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到update之前的状态。

持久性及实现:redo log

1. 定义

持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

2. 实现原理:redo log

redo log和undo log都属于InnoDB的事务日志。下面先聊一下redo log存在的背景。

InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。为此,InnoDB提供了缓存(Buffer Pool),Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。

Buffer Pool的使用大大提高了读写数据的效率,但是也带了新的问题:如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。

于是,redo log被引入来解决这个问题:当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作;当事务提交时,会调用fsync接口对redo log进行刷盘(这个刷盘指写入到redo log)。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。

既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:

(1)刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。

(2)刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。

3. redo log与binlog

我们知道,在MySQL中还存在binlog(二进制日志)也可以记录写操作并用于数据的恢复,但二者是有着根本的不同的:

(1)作用不同:redo log是用于crash recovery的,保证MySQL宕机也不会影响持久性;binlog是用于point-in-time recovery的,保证服务器可以基于时间点恢复数据,此外binlog还用于主从复制。

(2)层次不同:redo log是InnoDB存储引擎实现的,而binlog是MySQL的服务器层(可以参考文章前面对MySQL逻辑架构的介绍)实现的,同时支持InnoDB和其他存储引擎。

(3)内容不同:redo log是物理日志,内容基于磁盘的Page;binlog的内容是二进制的,根据binlog_format参数的不同,可能基于sql语句、基于数据本身或者二者的混合。

(4)写入时机不同:binlog在事务提交时写入;redo log的写入时机相对多元:

  • 前面曾提到:当事务提交时会调用fsync对redo log进行刷盘;这是默认情况下的策略,修改innodb_flush_log_at_trx_commit参数可以改变该策略,但事务的持久性将无法保证。
  • 除了事务提交时,还有其他刷盘时机:如master thread每秒刷盘一次redo log等,这样的好处是不一定要等到commit时刷盘,commit速度大大加快。

事务执行过程

在数据库的世界里,数据从来都不重要,日志才是最重要的,有了日志就有了一切。

事务执行流程

img

  1. 开始事务。
    1. 当用户执行 BEGIN 或 START TRANSACTION 命令后,事务启动。
    2. InnoDB 会分配一个唯一的事务 ID (transaction ID),用于标识当前事务。
  2. 准备阶段。执行 SQL,修改数据并生成 redo log 和 undo log。用户执行一系列 INSERT、UPDATE、DELETE 或 SELECT 操作。这些操作会被记录到 redo log (重做日志) 和 undo log (回滚日志) 中,以支持崩溃恢复和事务回滚。事务的修改内容存储在 buffer pool (内存中的数据页) 中,尚未写入磁盘。
    • 先记录 undo log 到log buffer。 记录逻辑日志,用于支持回滚操作,确保事务的原子性.
    • 修改 Buffer Pool:
      • 数据修改会先应用到内存中的数据页(Buffer Pool)。是个缓存。
      • 被修改的数据页会被标记为 “脏页”(Dirty Page),等待后续异步刷盘操作。
    • 生成redo log:
      • redo log:记录物理日志,用于崩溃恢复,确保即使系统宕机也可以通过重做操作恢复数据一致性。
    • 事务未提交:
      • SQL 执行过程中,事务仍处于活跃状态,未进行提交操作。
      • 数据的修改仅在事务内部可见,其他事务无法看到。
  3. 事务提交
    1. Prepare 阶段(两阶段提交的第一阶段):写入事务的修改记录和 prepare 标记。事务在提交时,先进入 Prepare 阶段,确保所有的日志记录都写入了持久存储。
      • 写入 redo log:
        • 将当前事务的所有 redo log 记录从 redo log buffer 刷入磁盘(或根据参数配置写入)。
        • 写入日志时,标记事务为“准备提交”状态(Prepare 状态)。
      • 更新 binlog。
        • 调用fsync,将binlog从file cache写到磁盘,这是由sync_binlog参数控制的
    2. Commit 阶段:写入事务的 commit 标记,完成提交。、
      • 在 redo log 中写入commit记录。引擎提交后会清除undolog。
      • 此时,该状态并不需要持久化到磁盘,只需要 write() 到操作系统的 Page Cache 中即可。因为,只要 binlog 写磁盘成功,就算 redo log 的状态还是 prepare 也没有关系,一样会被认为事务已经执行成功。在 MySQL 重启后,会按顺序扫描 redo log 文件,找到处于 prepare 状态的 redo log 写入事务,就使用 redo log 中的 XID 去 binlog 查看是否存在此 XID,binlog没有就回滚,有就提交。

img

undo log -> buffer pool 修改 -> redo log 刷盘 prepare -> binlog -> redo log 更新为提交。

注意undo log不需要2pc。

2PC

redo log和binlog两者是2PC控制的,先redo log prepare,再更新binlog,然后redo log commit并进行持久化。

  • 因为 redo log 影响主库的数据,binlog 影响从库的数据,所以, redo log 和 binlog 必须保持一致才能保证主从数据一致。
  • 事务的提交过程有两个阶段,就是将 redo log 的写入拆成了两个步骤:prepare 和 commit,中间再穿插写入 binlog 的步骤。
  • 事务没提交的时候,redo log 也是可能被持久化到磁盘的。redo log有参数可以每秒异步刷盘。

redo log的写入机制

  • 为了控制 redo log 的写入策略,InnoDB 提供了 innodb_flush_log_at_trx_commit 参数,它有三种可能取值,默认为1。
    • 设置为 0 的时候,表示每次事务提交时都只是把 redo log 留在 redo log buffer 中 ;
    • 默认的情况. 设置为 1 的时候,表示每次事务提交时都将 redo log 直接持久化到磁盘;
    • 设置为 2 的时候,表示每次事务提交时都只是把 redo log 写到 page cache
  • 如果是0或者2,则InnoDB 有一个后台线程,每隔 1 秒,就会把 redo log buffer 中的日志,调用 write 写到文件系统的 page cache,然后调用 fsync 持久化到磁盘

崩溃恢复机制

如果系统崩溃或掉电,可能出现以下情况:

  • Buffer Pool 中的数据页被修改,但还没来得及把 redo log 刷到磁盘。
  • 这种情况是崩溃恢复需要处理的问题。

崩溃恢复机制

  • 当数据库重启时,InnoDB 会通过 redo log 和 undo log 恢复一致性。

  • 两种可能性

    • redo log 缺失:因为 redo log 未持久化,则对应事务被视为未提交,数据不会被刷入磁盘。
    • redo log 已持久化:即使 Buffer Pool 数据丢失,InnoDB 也能通过 redo log 重做操作,恢复数据。

事务由存储引擎实现

MySQL中服务器层不管理事务,事务是由存储引擎实现的。MySQL支持事务的存储引擎有InnoDB、NDB Cluster等,其中InnoDB的使用最为广泛;其他存储引擎不支持事务,如MyIsam、Memory等。

如无特殊说明,后文中描述的内容都是基于InnoDB。

自动提交和手动提交

手动提交

采用了start transaction就会手动提交。

典型的MySQL事务是如下操作的:

start ``transaction``;``…… #一条或多条sql语句``commit``;

其中start transaction标识事务开始,commit提交事务,将执行结果写入到数据库。如果sql语句执行出现问题,会调用rollback,回滚所有已经执行成功的sql语句。当然,也可以在事务中直接使用rollback语句进行回滚。

自动提交

MySQL 默认采用自动提交模式。也就是说,如果不显式使用START TRANSACTION语句来开始一个事务,那么每个查询操作都会被当做一个事务并自动提交。

通过如下方式,可以关闭autocommit;需要注意的是,autocommit参数是针对连接的,在一个连接中修改了参数,不会对其他连接产生影响。

img

如果关闭了autocommit,则所有的sql语句都在一个事务中,直到执行了commit或rollback,该事务结束,同时开始了另外一个事务。

特殊操作强制执行commit

在MySQL中,存在一些特殊的命令,如果在事务中执行了这些命令,会马上强制执行commit提交事务;如DDL语句(create table/drop table/alter/table)、lock tables语句等等。

不过,常用的select、insert、update和delete命令,都不会强制提交事务。

隔离性 (重要,解决并发问题)

定义

与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响。隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。

隔离性追求的是并发情形下事务之间互不干扰。简单起见,我们主要考虑最简单的读操作和写操作(加锁读等特殊读操作会特殊说明),那么隔离性的探讨,主要可以分为两个方面:

  • (一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性

    • 写操作一定会加上行锁。
  • (一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性

    • 读可以不用加锁。

隔离性 MVCC实现: 非加锁读即快照读

MVCC 介绍

RR解决脏读、不可重复读、幻读等问题,使用的是MVCC:MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议。

MVCC的特点:在同一时刻,不同的事务读取到的数据可能是不同的(即多版本)——在T5时刻,事务A和事务C可以读取到不同版本的数据。

img

MVCC最大的优点是读不加锁,因此读写不冲突,并发性能好。InnoDB实现MVCC,多个版本的数据可以共存,主要基于以下技术及数据结构:

1)隐藏列:InnoDB中每行数据都有隐藏列,隐藏列中包含了本行数据的事务id、指向undo log的指针等。

2)基于undo log的版本链:前面说到每行数据的隐藏列中包含了指向undo log的指针,而每条undo log也会指向更早版本的undo log,从而形成一条版本链。

3)ReadView:通过隐藏列和版本链,MySQL可以将数据恢复到指定版本;但是具体要恢复到哪个版本,则需要根据ReadView来确定。所谓ReadView,是指事务(记做事务A)在某一时刻给整个事务系统(trx_sys)打快照,之后再进行读操作时,会将读取到的数据中的事务id与trx_sys快照比较,从而判断数据对该ReadView是否可见,即对事务A是否可见。

事务运行过程中,不会看到其他事务提交的新数据(即使另一个事务提交了)。

实现

在实现上, InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。

数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。

这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。

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

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

在实现上, InnoDB 为每个事务构造了一个数组,用来保存这个事务启动瞬间,当前正在“活跃”的所有事务 ID。“活跃”指的就是,启动了但还没提交。

数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。

这个视图数组和高水位,就组成了当前事务的一致性视图(read-view)。

而数据版本的可见性规则,就是基于数据的 row trx_id 和这个一致性视图的对比结果得到的。

这个视图数组把所有的 row trx_id 分成了几种不同的情况。

image-20210107105817266

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

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

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

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

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

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

MVCC RR级别下解决赃读、不可重复读、幻读

Read Repeatable隔离级别:开启事务后第⼀个select语句才是快照读的地⽅,⽽不是⼀开启事务就快照读。

下面以RR隔离级别为例,结合前文提到的几个问题分别说明。

(1)脏读

img

当事务A在T3时刻读取zhangsan的余额前,会生成ReadView,由于此时事务B没有提交仍然活跃,因此其事务id一定在ReadView的rw_trx_ids中,因此根据前面介绍的规则,事务B的修改对ReadView不可见。接下来,事务A根据指针指向的undo log查询上一版本的数据,得到zhangsan的余额为100。这样事务A就避免了脏读。

(2)不可重复读

img

当事务A在T2时刻读取zhangsan的余额前,会生成ReadView。此时事务B分两种情况讨论,一种是如图中所示,事务已经开始但没有提交,此时其事务id在ReadView的rw_trx_ids中;一种是事务B还没有开始,此时其事务id大于等于ReadView的low_limit_id。无论是哪种情况,根据前面介绍的规则,事务B的修改对ReadView都不可见。

当事务A在T5时刻再次读取zhangsan的余额时,会根据T2时刻生成的ReadView对数据的可见性进行判断,从而判断出事务B的修改不可见;因此事务A根据指针指向的undo log查询上一版本的数据,得到zhangsan的余额为100,从而避免了不可重复读。

(3)幻读

img

MVCC避免幻读的机制与避免不可重复读非常类似。

当事务A在T2时刻读取0<id<5的用户余额前,会生成ReadView。此时事务B分两种情况讨论,一种是如图中所示,事务已经开始但没有提交,此时其事务id在ReadView的rw_trx_ids中;一种是事务B还没有开始,此时其事务id大于等于ReadView的low_limit_id。无论是哪种情况,根据前面介绍的规则,事务B的修改对ReadView都不可见。

当事务A在T5时刻再次读取0<id<5的用户余额时,会根据T2时刻生成的ReadView对数据的可见性进行判断,从而判断出事务B的修改不可见。因此对于新插入的数据lisi(id=2),事务A根据其指针指向的undo log查询上一版本的数据,发现该数据并不存在,从而避免了幻读。

读已提交(RC)隔离级别下的MVCC非加锁读

RC与RR一样,都使用了MVCC,其主要区别在于:

RR是在事务开始后第一次执行select前创建ReadView,直到事务提交都不会再创建。根据前面的介绍,RR可以避免脏读、不可重复读和幻读。

RC每次执行select前都会重新建立一个新的ReadView,因此如果事务A第一次select之后,事务B对数据进行了修改并提交,那么事务A第二次select时会重新建立新的ReadView,因此事务B的修改对事务A是可见的。因此RC隔离级别可以避免脏读,但是无法避免不可重复读和幻读。

隔离性 锁实现: 加锁读即当前读

锁机制

首先来看两个事务的写操作之间的相互影响。隔离性要求同一时刻只能有一个事务对数据进行写操作,可以通过锁机制来保证这一点。

锁机制的基本原理可以概括为:事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。

行锁与表锁

按照粒度,锁可以分为表锁、行锁以及其他位于二者之间的锁。表锁在操作数据时会锁定整张表,并发性能较差;行锁则只锁定需要操作的数据,并发性能好。但是由于加锁本身需要消耗资源(获得锁、检查锁、释放锁等都需要消耗资源),因此在锁定数据较多情况下使用表锁可以节省大量资源。MySQL中不同的存储引擎支持的锁是不一样的,例如MyIsam只支持表锁,而InnoDB同时支持表锁和行锁,且出于性能考虑,绝大多数情况下使用的都是行锁。

锁机制实现隔离性

按照是否加锁,MySQL的读可以分为两种:

一种是非加锁读,也称作快照读、一致性读,使用普通的select语句,这种情况下使用MVCC避免了脏读、不可重复读、幻读,保证了隔离性。

另一种是加锁读,查询语句有所不同,如下所示:

#共享锁读取

``select``...lock ``in` `share mode``

#排它锁读取

``select``...``for` `update

加锁读在查询时会对查询的数据加锁(共享锁或排它锁)。由于锁的特性,当某事务对数据进行加锁读后,其他事务无法对数据进行写操作,因此可以避免脏读和不可重复读。而避免幻读,则需要通过next-key lock。next-key lock是行锁的一种,实现相当于record lock(记录锁) + gap lock(间隙锁);其特点是不仅会锁住记录本身(record lock的功能),还会锁定一个范围(gap lock的功能)。因此,加锁读同样可以避免脏读、不可重复读和幻读,保证隔离性。

注意:加锁读时不用使用MVCC, 因此如果在事务中第一次读取采用非加锁读,第二次读取采用加锁读,则如果在两次读取之间数据发生了变化,两次读取到的结果不一样,因为加锁读时不会采用MVCC。

可重复读情况下,加锁读时就会加锁,直到事务提交才解锁。

隔离性小结

概括来说,InnoDB实现的RR,通过锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView)等,实现了一定程度的隔离性,可以满足大多数场景的需要。

不过需要说明的是,RR虽然避免了幻读问题,但是毕竟不是Serializable,不能保证完全的隔离,下面是两个例子:

第一个例子,如果在事务中第一次读取采用非加锁读,第二次读取采用加锁读,则如果在两次读取之间数据发生了变化,两次读取到的结果不一样,因为加锁读时不会采用MVCC。

第二个例子,如下所示,大家可以自己验证一下。

img

一致性

1. 基本概念

一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)。

2. 实现

可以说,一致性是事务追求的最终目标:前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。

实现一致性的措施包括:

  • 保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证
  • 数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等
  • 应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致

QA

总结一下ACID特性及其实现原理:

  • 原子性:语句要么全执行,要么全不执行,是事务最核心的特性,事务本身就是以原子性来定义的;实现主要基于undo log
  • 持久性:保证事务提交后不会因为宕机等原因导致数据丢失;实现主要基于redo log
  • 隔离性:保证事务执行尽可能不受其他事务影响;InnoDB默认的隔离级别是RR,RR的实现主要基于锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView)
  • 一致性:事务追求的最终目标,不同事务访问结果一样。一致性的实现既需要数据库层面的保障,也需要应用层面的保障

补充:

  • 关于mysql RR到底可不可以解决幻读?

参考 和 参考例子

实际上,在RR下,select不会有phantom,读的是快照;可对于select for update(locking read)这种语句,如果前一次是select读快照(non-locking read),在RR下是可能会有幻像的,这因为select for update被认为是写。

RR级别下怎么解决幻读这个问题也得拆成快照读之间,当前读之间,快照读和当前读混合三部分。RR没有解决快照读/当前读混合下的幻读。

  1. RR隔离级别,如果事务中都是快照读,或者全都是当前读,都不会产生幻读。只有当前读和快照读混用,才会产生幻读。
  2. MVCC保证快照读不会幻读
  3. next-key lock保证当前读不会产生幻读

当 UPDATE 语句执行时,它会先读取数据的最新可见版本。

  • 如果数据已经被其他事务修改,则:
    • 乐观锁模式(默认):可能会导致 丢失更新(Lost Update),除非 WHERE 条件中带有 版本号 或 当前值 进行约束。
    • 悲观锁模式(SELECT ... FOR UPDATE):会阻塞其他事务,避免并发修改导致数据不一致。
  • 所以RR一直使用mvcc 快照读然后又有update是有问题的,应该加锁再update, 避免幻读以及丢失修改问题。

Search

    Table of Contents