Mysql二——事务控制背后的redo、undo以及MVCC

Redo日志

1、什么是redo日志

考虑到效率的问题,进行SQL操作的时候并不会直接将操作的数据写入到磁盘中而是会走一个缓存。既然有缓存的概念,那么就又缓存数据丢失造成与磁盘数据不一致的情况出现。这就是redo日志出现的背景,系统中将这个操作的相关记录计入到一个文件中,当系统发生问题的时候系统会保证这个文件可以被正常保存,当再次启动的时候就对这个文件进行一些操作保证恢复到系统发生异常前的状态。

2、redo简单日志类型

  • 简单日志类型的场景:

在Mysql中的上一篇文章中我们介绍了Mysql中页的概念在其中有记录的概述,每条记录中分为额外信息、真实数据两部分。其中正式数据中除过有真实的信息还有trx_id、roll_pointer两个隐藏字段。在有一种情况下还有这一个隐藏字段row_id,这个隐藏的字段是为了表示一条几率的唯一性。有这个row_id场景的就是表中既没有主键、也没有非空的unique字段。那么这个row_id的字段是从哪里来的呢?

这个row_id的字段是通过一个全局变量Max_Row_Id,每次需要row_id的时候就获取Max_Row_Id的值,并对它加一。如果数据库重启或是发生了其他的操作内容中的Max_Row_Id就会消失,所以每当Max_Row_Id的值是256的整数倍时候,系统就会将Max_Row_Id写到磁盘中。但是这时候如果系统发生了异常该怎么办呢,就像当Max_Row_Id 是300的时候系统发生了重启,这时候磁盘中存的Max_Row_Id是256,如果这时候用这个值的时候就会由部分的row_id重复从而造成插入的失败,这时候系统会将从磁盘中拿到的Max_Row_Id+256再放到缓存中作为全局的使用,这时候虽然发生了浪费但是起码保证了系统的正确性。

针对Row_Id这种方式记录的redo日志就是简单日志,从上面的介绍中也可以看出来这时候是没有聚簇索引、二级索引所引起的对B、B+树等索引结构维护的,这样场景下记录的Redo日志我们称之为简单日志类型。
Mysql二——事务控制背后的redo、undo以及MVCC_第1张图片
有简单的日志类型那么就对应的有复杂日志类型:
Mysql二——事务控制背后的redo、undo以及MVCC_第2张图片

3、Redo日志组

在系统中一个日志组对应一个原子操作,程序会读取一条条的redo日志直到读取到了结束标识类型的redo日志 ,但是有时候一个日志组只有一条日志,如果我们每一个单条的日志组都也设置结尾标识类型的redo日志,那么将会很多大的浪费空间。系统中对于这种情况是利用了日志中type字段中的一个bit来做这个标识。
Mysql二——事务控制背后的redo、undo以及MVCC_第3张图片
如果要是连续的日志一直读也没有读取到结束标志日志,这时候就不会进行恢复操作,因为这并不是一个完整的日志组,反正就算是利用redo的日志进行了恢复也是恢复错,那就还不如不进行恢复。

日志组的概念大家听起来可能爱还是有些陌生,这时候引入最小事务MTR以及事务、sql语句、最小事务、redo日志的架构就可以有一个清晰的认识。

4、MTR

对底层页面进行一次原子访问的过程被称为一次MTR,他的全称是Mini-Trascation:
Mysql二——事务控制背后的redo、undo以及MVCC_第4张图片

5、Redo log block

redo相对于Redo log block就像记录相对于页
Mysql二——事务控制背后的redo、undo以及MVCC_第5张图片
_HDR_DATA_LEN:初始的时候是12字节也就是block header,当快满的时候会加上block trailer,长度变为512。类比于记录中页以及Buffer Pool的概念,连续的block组成的就是log Buffer,他是一篇连续的存储空间:
Mysql二——事务控制背后的redo、undo以及MVCC_第6张图片
在这个过程中重要要理解的一点是Redo log写入log buffer的一个先后循序,以及在多事务下的一个情况,有关log buffer的相关触发过程可以理解为:

开始事务——>执行sql——>mtr——>redo log——block——log buffer
Mysql二——事务控制背后的redo、undo以及MVCC_第7张图片
写入到log Buffer的日志最终是需要写入到磁盘上的,那么磁盘上有关log的日志是怎样的呢?又是什么时候刷新到磁盘上的?

6、Redo日志的刷盘

在Mysql的数据目录中,默认的情况下有ib_logfile0和ib_logfile1的两个文件,log buffer中的日志在默认情况下就是刷新到这两个磁盘中。但是关于有多少个这样的日志文件以及日志文件的大小时怎样的都是可以通过配置项来进行设置的。在刷新的时候日志文件是进行顺序循环的刷新,在下面五中情况进行刷新:

1、log buffer空间不足50%的时候

2、事务提交的时候

3、后台有一个线程,大约一每一秒的额频率建log buffer中的redo日志刷新到磁盘

4、正常关闭服务器

5、做checkpoint时

在这里需要注意的一点就是事务提交的时候buffer pool中页是未必刷新到磁盘中的,这时候我们的日志文件必须要保证刷新到磁盘中,否则这时候如果发生什么异常就会导致看着似乎数据成功的保存了但是实际上并没有保存的情况。
Mysql二——事务控制背后的redo、undo以及MVCC_第8张图片

7、磁盘中Redo文件的格式

这一部分要关注的一点就是存储的形式,这里也是以512B问单位进行的,这样的存储形式对应于磁盘中的redo block,主要也是为了便于管理。 这里说的磁盘日志文件就是上面我们说到的ib_logfile文件,这些文件的存储位置默认从2048B开始。
Mysql二——事务控制背后的redo、undo以及MVCC_第9张图片

8、lsn(Log sequence number)与 redo日志刷新磁盘长度

lsn是内存中的相关概念,其中这一步重要的是有关lsn值的计算,在计算这些值的时候我们需要考虑到header、trailer的概念,当跨block的时候要将这些值计算进去
Mysql二——事务控制背后的redo、undo以及MVCC_第10张图片
看到这里不仅想到,在buffer pool中我们缓存与磁盘的交互是以页为单位的那么在redo中磁盘与缓存的交互是以block为主呢还是以mtr为主
Mysql二——事务控制背后的redo、undo以及MVCC_第11张图片

Undo日志

在系统中事务可能会出现异常或是其他的情况,为了保证事务的一致性我们记录下来的信息就是说Undo日志.Undo日志是为了保证事务的一致性,那么对数据会造成不一致的操作没有select,只有update、insert、delect。所以在下面讲到的Undo日志就是围绕这三种展开的

1、事务id

  • 什么是事务di

undo日志中的事务可以分为两类,读事务、读写事务。读事务一般来说针对得是临时表的情况,在这样的事务中是只允许读操作,读写事务就是我们正常对表的操作,在这样的事务中既可以读也可以写。当涉及对临时表进行增删改操作的时候也是需要进行分配事务id的。

  • 什么时候分配事务id?

当对数据建库记性增删改操作的时候分配事务id,如果事务知识查询那么事务id默认的是0

  • 如何开启事务id?

    只读事务:START TRANSITIONT READ ONLY

    读写事务:BEGIN;START TRANSITION

  • 如何维护事务id?

    事务id的维护与前面说的row_id的维护是相同的,他是有一个全局变量MAX_TRX_ID,他的持久化规则与row_id 对应的MAX_ROW_ID也是一样的,也是以256为维度的

2、三种类型的Undo日志

  • select类型的Undo日志

Mysql二——事务控制背后的redo、undo以及MVCC_第12张图片

  • delete类型的Undo日志

Mysql二——事务控制背后的redo、undo以及MVCC_第13张图片
在进行delect 的时候记录变化的一个情况,这里的删除是一个逻辑删除而且他是一个渐进的过程,并非说删除就立马删除了:
Mysql二——事务控制背后的redo、undo以及MVCC_第14张图片
len_of_index_col_info中值是索引列各列信息列表中长度综合,结合上面的信息我们知道为:1+1+4+1+1+4=12,那为什么为14呢,这是因为这个字段自身还有两个长度。

  • update类型的undo日志

Mysql二——事务控制背后的redo、undo以及MVCC_第15张图片

事务

这里说的是吴就是我们常熟的ACID即原子性、一致性、隔离性、永久性:

原子性Atomicity:一个事务中的SQL要么全部成功要么全部失败

一致性Consistency:一致性一般来说是从代码层面来进行控制的,例如人民币的额度不能是负数、地址不能为空、性别只有男女

隔离性Isolation:当存在多个线程多个事务访问相同数据的时候,要让他们之间不受影响

永久性Durability:现实中显示已经转换到数据库中的数据一定要保证到保存到磁盘上,这里可能就是redo日志存在的意义

1、事务中存在的状态

Mysql二——事务控制背后的redo、undo以及MVCC_第16张图片

2、并发时候事务一致性问题

并发情况下事务一致性问题可以总结为脏写、脏读、不可重复度、幻读,解决这些问题需要采用不同的隔离级别:

  • 脏写

多个不同的事务可以对相同的数据进行任意的更改,这种情况系统是不允许发生的

  • 脏读

一个事务写完之后还没来得及提交数据到磁盘,这时候他的数据被另一个数据读取到了,但写的那个事务失败最终数据没有写入到磁盘的时候,这时候第二个事务就发生了脏读。

  • 不可重复度

如果一个事务修改了另一个事务尚未提交的数据,这时候在同一个事务中前后两次读取到的值是不一样的,这样的现象就是不可重复度

脏读与不可重重复读的区别在于,脏读是读取了一个因为各种原因没有保存的数据;不可重复读是在他读的时候数据被改变了所以在数据被改变的前后数据的值不一样
Mysql二——事务控制背后的redo、undo以及MVCC_第17张图片

  • 幻读

幻读与不可重复读是有些像的,不同之处在于幻读查询的时候添加了查询条件,而另一个数据在这个过程中插入的数据刚好满足查询事件中的查询条件,这时候前后发生的数据不一致就是幻读。不可重复读他们影响的是同一条数据,但是幻读是其余的事务添加了其他的记录
Mysql二——事务控制背后的redo、undo以及MVCC_第18张图片

  • 事务的隔离级别

隔离级别 脏读 不可重复度 幻读
read_uncommit Y Y Y
read_commit N Y Y
repeatable_read N N Y
serializable N N N

MVCC

四种事务隔离级别种read_uncommit的隔离界别太低一致性差,serializable的隔离级别高一致性好但是性能差所以在系统中我们常用的隔离级别是read_commit,repeatable_read这两种。关于这两种隔离级别的实现就是我们这里要说的MVCC(Multi-Version Concurrency Control——多版本并发控制)。他是利用版本连与ReadView来记录多个事务并发对相同记录记性访问的行为。这里说的ReadView我们可以理解为是系统的一个快照,这个快照可以用来判断版本链中的那个版本对于当前事务是可见的。

1、版本链

Mysql二——事务控制背后的redo、undo以及MVCC_第19张图片

2、ReadView

ReadView相当于是一个快照,他表示的是在生成readview的时候一些变量的值,下面的介绍种说到的活跃读写事务是哪些对数据库数据有改变但是还没有提交或是回滚的事务:

m_ids:生成ReadView时,系统中活跃的读写事务id列表

min_trx_id:生成ReadView时,系统中m_ids中事务id最小的值

max_trx_id:生成ReadView时,系统要分给下一个事务的id值

creator_trx_id:生成ReadView时的事务id

Mysql二——事务控制背后的redo、undo以及MVCC_第20张图片

3、React_Commited 与 Repeated_Read 区别于MVCC实例解析

这两个不同的隔离级别最大的一个区别就是他们生成的ReadView的时机不同,READ COMMITTED隔离级别下,每次读取数据都生成一个ReadView;REPEATABLE READ 隔离级别下,在一个事务中,只在第一次读取数据的时候生成ReadView。

期初听到数这两个隔离级别的时候感觉像时听错了,怎么“快照”生成的越少,事务的一致性反而越好呢?然而事实却是如此,在这里我们可以简单的理解为如果快照读取的太快,他就会过早的读取到一些数据。这两个隔离级别分别是为了解决脏读与不可重复读的数据,那么他们是如何解决的呢?

  • 两个隔离级别对脏读的解决

脏读就是一个事务读取了一个还没有被提交的数据,那么我们只需要解决这个问题就可以解决脏读的问题了。

在READ_COMMITTED时候,每次读之前都会生成一个ReadView,而那个修改还没提交的事务id就会作为一个活跃的读写事务存在于m_ids。这在MVCC的定义中这个事务对应的版本自然就对当前当前事务不可见了。

在REPEATABLE_READ时候,只在第一次读取的时候生成ReadView,关于第一次的实现就不用说了,安为什么在后面的select操作中也可以实现对脏读的约束呢?在同一个事务中后面的select采用的都是第一次的,第一次的时候就判断了那个修改事务对应的版本不可见,后面就算是那个修改事务的提交了,他也默认的还是没有第一次的没有提交的状态,所以后续的select还是可以实现对事务的脏读控制

  • 两个隔离界别对不可重复度问题的解决

不可重复读就是在一个事务中第一次读取的时候是正常的但是第二次读取的时候数据被修改并且提交了,所以第二次读到的就是一个被修改过的值。

在READ_COMMITTED时候,是每次读之前都会生成一个ReadView,对于读事务的第一个select自不用说,但是当第二个select的时候,他有重新的生成了readview,在这个readview中刚刚修改记录那个事务的trx_id是不再m_ids中的而是属于trx_id not in m_ids,条件成立所以他对应的版本对于当前的select事务是可见的,这时候与第一次读取到的记录值就是不相同的,这样的情况就是不可重复度,所以READ_COMMITTED不能预防不可重复读

在REPEATABLE_READ时候,只在第一次读取的时候生成ReadView,这里第一次的时候不用考虑,但是对于第二次因为他采用的还是第一次的ReadView,第一次的时候写改事务还没有发生所以写改事务的trx_id的区间是trx_id>=max_trx_id。对于第二次事务属于不可见版本,所以在此隔离级别下是可以实现可重复读的。
与第一次读取到的记录值就是不相同的,这样的情况就是不可重复度,所以READ_COMMITTED不能预防不可重复读

在REPEATABLE_READ时候,只在第一次读取的时候生成ReadView,这里第一次的时候不用考虑,但是对于第二次因为他采用的还是第一次的ReadView,第一次的时候写改事务还没有发生所以写改事务的trx_id的区间是trx_id>=max_trx_id。对于第二次事务属于不可见版本,所以在此隔离级别下是可以实现可重复读的。

你可能感兴趣的:(大工篇,redo日志,undo日志,MVCC,Mysql事务控制原理)