lukaliou123 / lukaliou123.github.io

lukaliou123在2022年的面试用知识点总结
Other
5 stars 0 forks source link

MySql--事务实现原理和MVCC #16

Open lukaliou123 opened 2 years ago

lukaliou123 commented 2 years ago

1.DDL语句、DML语句与DCL语句

SQL 语句主要可以划分为以下 3 个类别。

DDL(Data Definition Languages)语句:数据定义语言,这些语句定义了不同的数据段、数据库、表、列、索引等数据库对象的定义。常用的语句关键字主要包括 create、drop、alter等。

DML(Data Manipulation Language)语句:数据操纵语句,用于添加、删除、更新和查询数据库记录,并检查数据完整性,常用的语句关键字主要包括 insert、delete、udpate 和select 等。(增添改查)

DCL(Data Control Language)语句:数据控制语句,用于控制不同数据段直接的许可和访问级别的语句。这些语句定义了数据库、表、字段、用户的访问权限和安全级别。主要的语句关键字包括 grant、revoke 等。

lukaliou123 commented 2 years ago

2.原子性(Atomicity)实现原理:undo log

MySQL 的事务日志 MySQL 的日志有很多种,如二进制日志、错误日志、查询日志、慢查询日志等,此外 InnoDB 存储引擎还提供了两种事务日志:redo log(重做日志)和undo log(回滚日志)。其中,redo 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之前的状态。

lukaliou123 commented 2 years ago

3.持久性(Durability)实现原理:redo log

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进行刷盘。如果 MySQL 宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是 WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求。

既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因: ● 刷脏是随机 IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序 IO。 ● 刷脏是以数据页(Page)为单位的,MySQL 默认页大小是 16 KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效 IO 大大减少

lukaliou123 commented 2 years ago

4.binlog和redolog对比

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

作用不同redo log是用于crash recovery的,保证 MySQL 宕机也不会影响持久性;binlog是用于point-in-time recovery的,保证服务器可以基于时间点恢复数据,此外binlog还用于主从复制。 ● 层次不同:redo log是 InnoDB 存储引擎实现的,而binlog是 MySQL 的服务器层实现的,同时支持 InnoDB 和其他存储引擎。 ● 内容不同:redo log是物理日志,内容基于磁盘的Page;binlog的内容是二进制的,根据binlog_format参数的不同,可能基于 SQL 语句、基于数据本身或者二者的混合。 ● 写入时机不同:binlog在事务提交时写入;redo log的写入时机相对多元: ○ 前面曾提到当事务提交时会调用fsync对redo log进行刷盘,这是默认情况下的策略,修改innodb_flush_log_at_trx_commit参数可以改变该策略,但事务的持久性将无法保证。 ○ 除了事务提交时,还有其他刷盘时机,如master thread每秒刷盘一次redo log等,这样的好处是不一定要等到commit时刷盘,commit速度大大加快。

lukaliou123 commented 2 years ago

4.隔离性实现原理:锁和MVCC

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

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

锁机制

隔离性要求同一时刻只能有一个事务对数据进行写操作,InnoDB 通过锁机制来保证这一点。

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

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

1648542533(1)

lukaliou123 commented 2 years ago

5.MVCC

RR解决脏读、不可重复读、幻读等问题,使用的是 MVCC:MVCC 全称Multi-Version Concurrency Control,即多版本的并发控制协议。下面的例子很好的体现了 MVCC 的特点:在同一时刻,不同的事务读取到的数据可能是不同的(即多版本)—— 在 T5 时刻,事务A和事务C可以读取到不同版本的数据。 image

同一行数据平时发生读写请求时,会上锁阻塞住。但mvcc用更好的方式去处理读—写请求,做到在发生读—写请求冲突时不用加锁。

这个读是指的快照读,而不是当前读,当前读是一种加锁操作,是悲观锁。

当前读 它读取的数据库记录,都是当前最新的版本,会对当前读取的数据进行加锁,防止其他事务修改数据。是悲观锁的一种操作。 如下操作都是当前读:

select lock in share mode (共享锁)

select for update (排他锁)

update (排他锁)

insert (排他锁)

delete (排他锁)

串行化事务隔离级别

快照读 快照读的实现是基于多版本并发控制,即MVCC,既然是多版本,那么快照读读到的数据不一定是当前最新的数据,有可能是之前历史版本的数据。

如下操作是快照读:

不加锁的select操作(注:事务级别不是串行化

快照读与mvcc的关系 MVCCC是“维持一个数据的多个版本,使读写操作没有冲突”的一个抽象概念

这个概念需要具体功能去实现,这个具体实现就是快照读

lukaliou123 commented 2 years ago

6.MVCC解决并发哪些问题?

mvcc用来解决读—写冲突的无锁并发控制,就是为事务分配单向增长时间戳。为每个数据修改保存一个版本,版本与事务时间戳相关联。

读操作只读取该事务开始前的数据库快照。 解决问题如下:

并发读-写时:可以做到读操作不阻塞写操作,同时写操作也不会阻塞读操作。

解决脏读、幻读、不可重复读等事务隔离问题,但不能解决上面的写-写 更新丢失问题。

因此有了下面提高并发性能的组合拳

MVCC + 悲观锁:MVCC解决读写冲突,悲观锁解决写写冲突

MVCC + 乐观锁:MVCC解决读写冲突,乐观锁解决写写冲突

lukaliou123 commented 2 years ago

7.MVCC的实现原理

它的实现原理主要是版本链undo日志Read View 来实现的

版本链 我们数据库中的每行数据,除了我们肉眼看见的数据,还有几个隐藏字段,得开天眼才能看到。分别是db_trx_iddb_roll_pointerdb_row_id

db_trx_id 6byte,最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID。

db_roll_pointer(版本链关键) 7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)

db_row_id 6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以db_row_id产生一个聚簇索引。

实际还有一个删除flag隐藏字段, 记录被更新或删除并不代表真的删除,而是删除flag变了

image

如上图,db_row_id是数据库默认为该行记录生成的唯一隐式主键,db_trx_id是当前操作该记录的事务ID,而db_roll_pointer是一个回滚指针,用于配合undo日志,指向上一个旧版本。

每次对数据库记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表,所以现在的情况就像下图一样: image

对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id,这个信息很重要,在根据ReadView判断版本可见性的时候会用到。

Undo log 的用途 保证事务进行rollback时的原子性和一致性,当事务进行回滚的时候可以用undo log的数据进行恢复。

用于MVCC快照读的数据,在MVCC多版本控制中,通过读取undo log的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本。

undo日志 Undo log 主要用于记录数据被修改之前的日志,在表信息修改之前先会把数据拷贝到undo log里。 当事务进行回滚时可以通过undo log 里的日志进行数据还原。

Undo log 的用途

保证事务进行rollback时的原子性和一致性,当事务进行回滚的时候可以用undo log的数据进行恢复。

用于MVCC快照读的数据,在MVCC多版本控制中,通过读取undo log的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本。

undo log主要分为两种:

insert undo log 代表事务在insert新记录时产生的undo log , 只在事务回滚时需要,并且在事务提交后可以被立即丢弃

update undo log(主要) 事务在进行update或delete时产生的undo log ; 不仅在事务回滚时需要,在快照读时也需要; 所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除

lukaliou123 commented 2 years ago

7.1.Read View(读视图)

事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照。 记录并维护系统当前活跃事务的ID(没有commit,当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以越新的事务,ID值越大),是系统中当前不应该被本事务看到的其他事务id列表。 Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。 Read View几个属性

trx_ids: 当前系统活跃(未提交)事务版本号集合。

low_limit_id: 创建当前read view 时“当前系统最大事务版本号+1”。

up_limit_id: 创建当前read view 时“系统正处于活跃事务最小版本号”

creator_trx_id: 创建当前read view的事务版本号;

image image image

db_trx_id < up_limit_id || db_trx_id == creator_trx_id(显示)

如果数据事务ID小于read view中的最小活跃事务ID,则可以肯定该数据是在当前事务启之前就已经存在了的,所以可以显示。

或者数据的事务ID等于creator_trx_id ,那么说明这个数据就是当前事务自己生成的,自己生成的数据自己当然能看见,所以这种情况下此数据也是可以显示的。

db_trx_id >= low_limit_id(不显示) 如果数据事务ID大于read view 中的当前系统的最大事务ID,则说明该数据是在当前read view 创建之后才产生的,所以数据不显示。如果小于则进入下一个判断

db_trx_id是否在活跃事务(trx_ids)中

不存在:则说明read view产生的时候事务已经commit了,这种情况数据则可以显示。

已存在:则代表我Read View生成时刻,你这个事务还在活跃,还没有Commit,你修改的数据,我当前事务也是看不见的。

补充:更好的说明和例子

1.rx_ids:它是一个列表,存储着在read view创建时,正在运行的、也就是“活跃的”事务的事务ID。这些事务在我们当前事务开始时是未提交状态,因此我们的事务不应该看到这些事务做出的任何修改

2.low_limit_id:这是一个值,当read view创建时,是“当前系统最大事务版本号+1”。这意味着所有比这个值小的事务ID,都已经在当前事务开始前提交了,因此当前事务可以看到它们的修改。

3.up_limit_id:这是一个值,它是在read view创建时,系统中正处于活跃状态的事务中的最小事务ID。这个值定义了一个界限,所有大于或等于这个值的事务ID,都是在当前事务开始后才启动的,因此当前事务不能看到它们的修改。

当一个事务要读取一行数据时,它会检查这行数据的事务ID,然后根据以下规则判断是否应该看到这行数据:

如果行的事务ID creator_trx_id在trx_ids列表中,那么这个修改对当前事务来说是不可见的,因为它是在当前事务开始后由一个还未提交的事务所做的。 如果行的事务ID creator_trx_id小于low_limit_id,那么这个修改对当前事务来说是可见的,因为它是在当前事务开始前就已经提交的。 如果行的事务ID creator_trx_id大于等于up_limit_id,那么这个修改对当前事务来说是不可见的,因为它是在当前事务开始后才开始的事务所做的。

一个例子:

事务1,事务ID:1,状态:已提交 事务2,事务ID:2,状态:已提交 事务3,事务ID:3,状态:活跃(未提交) 事务4,事务ID:4,状态:活跃(未提交) 此时,我们启动一个新的事务,事务5,事务ID:5。此时我们要创建一个read_view,这个是creator_trx_id

那么,根据定义,trx_ids将包括所有在创建read_view时活跃的事务的事务ID,因此trx_ids = [3, 4]low_limit_id是系统中当前最大的事务ID+1,因此low_limit_id = 5 + 1 = 6up_limit_id是系统中活跃的事务中的最小事务ID,因此up_limit_id = 3。 然后我们在事务5中开始读取数据:

当我们试图读取事务ID为1的行时,因为1小于low_limit_id(6),因此这个行对事务5是可见的。

当我们试图读取事务ID为2的行时,同样因为2小于low_limit_id(6),所以这个行对事务5也是可见的。

当我们试图读取事务ID为3的行时,因为3在trx_ids中,或者因为3等于up_limit_id,所以这个行对事务5是不可见的。

当我们试图读取事务ID为4的行时,因为4在trx_ids中,或者因为4大于up_limit_id(3),所以这个行对事务5是不可见的。

lukaliou123 commented 2 years ago

8.MVCC和事务隔离级别

上面所讲的Read View用于支持RC(Read Committed,读提交)RR(Repeatable Read,可重复读)隔离级别的实现。

RR、RC生成时机

RC隔离级别下,是每个快照读都会生成并获取最新的Read View;

而在RR隔离级别下,则是同一个事务中的第一个快照读*会创建Read View, 之后的快照读获取的都是同一个Read View,之后的查询就不会重复生成了,所以一个事务的查询结果每次都是一样的。

解决幻读问题

快照读:通过MVCC来进行控制的,不用加锁。按照MVCC中规定的“语法”进行增删改查等操作,以避免幻读。

当前读:通过next-key锁(行锁+gap锁)来解决问题的。

RC、RR级别下的InnoDB快照读区别

在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见

即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见

而在RC级别下的,事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因

lukaliou123 commented 2 years ago

9.总结

从以上的描述中我们可以看出来,所谓的MVCC指的就是在使用READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执行普通的SEELCT操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行从而提升系统性能。