什么是Undo log

Undo log是MySQL Innodb引擎的日志的一种,记录了老版本的数据
Undo log是Innodb MVCC重要组成部分,InnoDB的MVCC就是基于Undo log实现的。

MVCC(Multi-Version Concurrency Control | 多版本并发控制)
InnoDB通过为每一行记录添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。但是InnoDB并不存储这些事件发生时的实际时间,相反它只存储这些事件发生时的系统版本号(LSN)。这是一个随着事务的创建而不断增长的数字。每个事务在事务开始时会记录它自己的系统版本号。每个查询必须去检查每行数据的版本号与事务的版本号是否相同。

当我们对数据进行操作的时候,就会产生undo记录,Undo记录默认记录在系统表空间(ibdata)中,从MySQL 5.6开始,Undo使用的表空间可以分离为独立的Undo log文件

在Innodb当中,INSERT操作在事务提交前只对当前事务可见,Undo log在事务提交后即会被删除,因为新插入的数据没有历史版本,所以无需维护Undo log。而对于UPDATE、DELETE,责需要维护多版本信息。
在InnoDB当中,UPDATE和DELETE操作产生的Undo log都属于同一类型:update_undo。(update可以视为insert新数据到原位置,delete旧数据,undo log暂时保留旧数据)

Undo log的作用

有了MVCC,InnoDB就能实现一致性非锁定读

举个例子:

Session1(以下简称S1)和Session2(以下简称S2)同时访问(不一定同时发起,但S1和S2事务有重叠)同一数据A,S1想要将数据A修改为数据B,S2想要读取数据A的数据。

如果没有MVCC,那么事情的发展可能是这样的:

  1. S1先执行,修改数据A,数据页被锁,S2等待A修改完后读取新的数据。
  2. S2先执行,读取数据A,数据页被加读锁 ,S1想要加X锁失败,等待S2读取完毕后,修改数据A。
  3. S1、S2同时加锁,互斥,进入spin状态再重试,直到有一方加锁成功,后重现1或2。

如果,并发访问量不是2,而是两百、两千呢?
这无疑对数据库的性能有着非常严重的影响。

所以,InnoDB存储引擎通过多版本控制的方式来读取当前执行时间数据库中行的数据,如果读取的行正在执行DELETE或UPDATE操作,这是读取操作不会因此等待行上锁的释放。相反的,InnoDB会去读取行的一个快照数据(Undo log)。

在InnoDB当中,要对一条数据进行处理,会先看这条数据的版本号是否大于自身事务版本(非RU隔离级别下当前事务发生之后的事务对当前事务来说是不可见的),如果大于,则从历史快照(undo log链)中获取旧版本数据,来保证数据一致性。

而由于历史版本数据存放在undo页当中,对数据修改所加的锁对于undo页没有影响,所以不会影响用户对历史数据的读,从而达到非一致性锁定读,提高并发性能。

Undo log的结构

为了提高undo log的并发操作,InnoDB将Undo log拆分为很多的浮动程序段(relocatable segment)来进行维护,每个RSEG当中又有多个Undo log slot,每个事务占用一个slot。



mysql UNDO log 则么看 mysql undolog mvcc_undo-log



通过上图我们可以看到整个的undo log被分为了三个部分:
  • Rseg0
  • Rseg 1-32
  • Rseg 33 - 128

其中Rseg0预留在了系统表空间(ibdata)当中,Resg1-32这32个回滚段存放于临时表的系统表空间当中(在内存中,MySQL 5.7对于临时表空间做了优化),最后剩下的96个回滚段在MySQL5.6以后可以独立出ibdata文件,独立存放在undo表空间当中。如果我们不使用独立Undo表空间,那么它们依然会存放在ibdata当中。

每个回滚段当中都有一个断头页,在这个页上划分了1024个slot(TRX_RSEG_N_SLOTS)每个slot又对应到一个undo log对象,因此理论上InnoDB最多支持 96 * 1024个普通事务

关于undo段的分配,这里有一点需要注意的是:

如果我们使用独立Undo tablespace,则总是从第一个Undo space开始轮询分配undo 回滚段。大多数情况下这是OK的,但假设我们将回滚段的个数从33开始依次递增配置到128,就可能导致所有的回滚段都存放在同一个undo space中。(参考函数trx_sys_create_rsegs 以及 bug#74471)

分配Undo段

当开启一个事务(分为只读事务和读写事务)的时候,InnoDB会预先为事务分配一个回滚段:

  • 对于只读事务,如果产生对临时表的写入,责需要为其分配回滚段,使用临时表回滚段(Rseg1-32)。
  • 在MySQL5.7当中,事务默认以只读事务开启,当随后判定为读写事务时,责转换成读写模式,并为其分配事务ID和回滚段。

只读事务与读写事务的区别在于他们随后会不会记录redo log(undo也是需要redo来保护的)。

undo生命周期

在进行分配的时候,MySQL会从第一个回滚段开始轮询所有的回滚段,寻找当前不会被purge线程truncate掉的回滚段,以后该事务用到的undo page都会从这个undo段来分配。

然后在undo slot当中记录自己的事务ID,将该回滚段的count增加,来标识该回滚段中仍记录着未提交数据,防止被purge线程truncate掉。

最后,如果是临时表回滚段,则不记录redo,如果是普通读写操作,则会记录redo。

另外,如果一个事物,在只读阶段使用了临时表回滚段,之后又转变成了读写事物,那么两个回滚段都会被使用

事物提交之后,需要purge的undo段都会放到purge队列上。

Undo段的使用

undo在使用的时候会做以下两个判断:

  1. 用没用临时表
  2. INSERT还是UPDATE/DELETE

在源码当中,此处有个关键的函数:trx_undo_reuse_cached,这个函数是用来给事务分配slot的。

Undo在使用的时候,首先一定会从cache list找空闲的undo页进行分配(undo在满足一定条件的时候会把其拥有的undo页放到cached list当中,其他undo可以直接通过cached list来查找可复用的undo页而不需扫描所有的undo页寻找可用的slot,从而提高复用undo页的效率)

  • INSERT
  • 获取slot后修改头部重用信息(标识为不可重用),然后预留XID空间。
  • UPDATE/DELETE
  • 获取slot,并在undo log hdr page上创建新的Undo log header,预留XID空间。

被获取的undo页会从cached list上移除掉,并初始化该对象的信息,状态设置为被使用(TRX_UNDO_ACTIVE)。

如果没用从cache list上获取到undo页,则需要从undo段上分配一个空闲的undo slot,并创建对应的undo页,进行初始化。

已经分配给事物的undu页会加入到insert类或update类(包含delete)的list当中。
如果是数据词典操作(DDL)产生的undo,主要是表级别操作,例如创建或删除表,还需要记录操作的table id到undo log header中(TRX_UNDO_TABLE_ID),同时将TRX_UNDO_DICT_TRANS设置为TRUE。(trx_undo_mark_as_dict_operation)。

(未完待续)