事务回滚的需求:把为了回滚而记录的东西称为撤销日志,即undo日志

事务id

给事务分配id的时机

事务分为只读事务或者读写事务:

  • 只读事务中不可以对普通的表进行增、删、改操作,但可以对临时表做增、删、改操作。
  • 读写事务中可以对表执行增删改操作;

如果某个事务执行过程中对某个表执行了增删改操作,那么InnoDB存储引擎就会给它分配一个独一无二的事务id。

  • 对于只读事务来说,只有在它第一次对某个用户创建的链式表执行增删改操作的时候才会为这个事务分配一个事务id,否则不分配。
  • 对于读写事务来说,只有在它第一次对某个表执行增删改操作时才会为这个事务分配一个事务id,否则也是不分配。
事务id是怎么生成的

事务id本质山是一个数字,它的分配策略和row_id(隐藏列)的分配策略时相同的:

  • 服务器内部在内存中维护一个全局变量,每当需要为某一个事务分配事务id时,就会把该变量的值当做事务id分配给事务,并把该变量自增1;
  • 每当这个变量为256的倍数时,就会将该变量的值刷新到系统表空间的页号为5的页面中一个称之为Max Trx ID的属性处,这个属性占用8个字节存储。
  • 当系统下一次重启时,将这个Max Trx ID属性加载到内存,将该值加上256后赋值给上面的全局变量。

这样就可以保证整个系统中分配的事务id是一个递增的数字,先被分配id的事务得到较小的事务id,后被分配id的事务得到较大的事务id。

trx_id隐藏列

聚簇索引的记录除了会保存完整用户数据外,而且还会自动添加名为trx_id、roll_pointer的隐藏列,如果用户没有在表中定义主键以及Unique键,还会自动添加一个row_id的隐藏列。

mysql中undo log日志在哪里 mysql undo文件_mysql中undo log日志在哪里


其中trx_id就是某个对这个聚簇索引记录做改动的语句所在事务对应的事务id。

undo日志的格式

一般每对一条记录做一次改动,就对应着一条undo日志,一个事物在执行的过程中可能新增、删除、更新若干条记录,需要记录很多条对应的undo日志,这些undo日志会被从0开始编号,也就说根据生成顺序分别被称为第0号undo日志、第1号undo日志…,这个编号也被称为undo no

这些undo日志被记录到类型为FIL_PAGE_UNDO_LOG的页面中,这些页面可以从系统表空间分配,也可以从一种专门存放undo日志的表空间即undo tablespace中分配。

INSERT操作对应的undo日志

TRX_UNDO_INSERT_REC的undo日志:

mysql中undo log日志在哪里 mysql undo文件_链表_02

  • undo no在一个事务中是从0开始递增的,只要事务没提交,每生成一条undo日志,该日志的undo no就增1。
  • 如果记录中的主键值包含一个列,那么类型为TRX_UNDO_INSERT_REC的undo日志中只需要把该列占用的存储空间大小和真实值记录下来,如果记录中的主键包含多个列,那么每个列占用的存储空间大小和对应的真实值都需要记录下来。
roll_pointer隐藏列的含义

本质上就是指向记录对应的undo日志的一个指针,向表里插入2条记录,每条记录都有与其对应的一条undo日志,记录被存储到类型为FIL_PAGE_INDEX的页面中,undo日志被存放在类型为FIL_PAGE_UNDO_LOG的页面中。

mysql中undo log日志在哪里 mysql undo文件_主键_03


roll_pointer本质就是一个指针,指向记录对应的undo日志。

DELETE操作对应的undo日志

插入页面的记录会根据记录头信息中的next_record属性组成一个单向链表,称之为正常记录链表,被删除的的记录其实也会根据记录头信息中的next_record属性组成一个链表,只不过这个链表中的记录占用的存储空间可以被重新利用,所以成这个链表为垃圾链表。Page Header部分有一个称为PAGE_FREE的属性,它指向由被删除记录组成的垃圾链表的头节点。

mysql中undo log日志在哪里 mysql undo文件_链表_04


如果现在使用delete语句把正常记录链表中的最后一条记录给删除掉,这个过程需要经历两个阶段:

  • 阶段一:仅仅将delete_mask标识位设置为1,这个阶段称为delete mask
  • mysql中undo log日志在哪里 mysql undo文件_链表_05

  • 在删除语句所在的事务提交之前,被删除的记录一直都处于这种所谓的中间状态
  • 阶段二:当该删除语句所在的事务提交之后,会有专门的线程来真正把记录删除掉,其实就是把该记录从正常记录链表中移除,并加入到垃圾链表中,然后还要调整一些页面其他信息。这个阶段被称为purge

mysql中undo log日志在哪里 mysql undo文件_存储空间_06


被删除记录加入到垃圾链表时,实际上加入到链表的头节点处,会跟着修改PAGE_FREE属性的值。

页面的Page Header部分有一个PAGE_GARBAGE属性,该属性记录着当前页面中可重用存储空间占用的总字节数。每当有已删除记录被加入 到垃圾链表后,都会把这个PAGE_GARBAGE属性的值加上该已删除记录占用的存储空间大小。PAGE_FREE指向垃圾链表的头节点,之后每当新插入 记录时,首先判断PAGE_FREE指向的头节点代表的已删除记录占用的存储空间是否足够容纳这条新插入的记录,如果不可以容纳,就直接向页面中 申请新的空间来存储这条记录(是的,你没看错,并不会尝试遍历整个垃圾链表,找到一个可以容纳新记录的节点)。如果可以容纳,那么直接重用 这条已删除记录的存储空间,并且把PAGE_FREE指向垃圾链表中的下一条已删除记录。但是这里有一个问题,如果新插入的那条记录占用的存储空 间大小小于垃圾链表的头节点占用的存储空间大小,那就意味头节点对应的记录占用的存储空间里有一部分空间用不到,这部分空间就被称之为碎片 空间。那这些碎片空间岂不是永远都用不到了么?其实也不是,这些碎片空间占用的存储空间大小会被统计到PAGE_GARBAGE属性中,这些碎片空 间在整个页面快使用完前并不会被重新利用,不过当页面快满时,如果再插入一条记录,此时页面中并不能分配一条完整记录的空间,这时候会首先 看一看PAGE_GARBAGE的空间和剩余可利用的空间加起来是不是可以容纳下这条记录,如果可以的话,InnoDB会尝试重新组织页内的记录,重新组 织的过程就是先开辟一个临时页面,把页面内的记录依次插入一遍,因为依次插入时并不会产生碎片,之后再把临时页面的内容复制到本页面,这样 就可以把那些碎片空间都解放出来(很显然重新组织页面内的记录比较耗费性能)。

在删除语句所在的事务提交之前,只会经历阶段一,也就是delete mark阶段,因为事务提交之后就不同回滚了,所以只需考虑对删除操作的阶段一做的影响进行回滚。为此设计一种称之为TRX_UNDO_DEL_MARK_REC类型的undo日志。

mysql中undo log日志在哪里 mysql undo文件_存储空间_07

  • 在对一条记录进行delete mark操作前,需要把该记录的旧的trx_id和roll_pointer隐藏列的值都给记到对应的undo日志中来,就是图中显示的old trx_id和old roll_pointer属性。这样有一个好处就是可以通过undo日志的old roll_pointer找到记录在修改之前对应的undo日志。比如在一个事务中,先插入一条记录,然后又执行对该记录的删除操作。
  • mysql中undo log日志在哪里 mysql undo文件_主键_08

  • 执行完delete mark操作后,它对应的undo日志和insert操作对应的undo日志就串成一个链表,这个链表被称为版本链
  • 与类型TRX_UNDO_INSERT_REC的日志不同,TRX_UNDO_DEL_REC的undo日志还多了一个索引列各列信息的内容,也就是如果某个列被包含在某个索引中,那么它相关的信息就应该被记录到这个索引各列信息部分。所谓的相关信息包括该列在记录中的位置、该列占用的存储空间大小、该列的实际值。所以索引各列信息存储的内容实质上就是<pos,len,value>的一个列表,这部分信息主要用在事务提交后,对该中间状态记录做真正删除的阶段二时使用的。

mysql中undo log日志在哪里 mysql undo文件_mysql中undo log日志在哪里_09

UPDATE操作对应的undo日志

在执行update语句时,InnoDB对更新主键和不更新主键这两种方案有截然不同的处理。

不更新主键

在不更新主键的情况下,又可细分为被更新列占用存储空间发不发生变化两种情况。

  • 就地更新(in-place update)

对于被更新的没个列来说,如果更新后的列和更新前的列占用的存储空间都一样大,那么就可以进行就地更新。也就是直接在原记录的基础上修改对应列的值。特别注意必须是更新前后占用存储空间一样大。

  • 先删除旧记录,在插入新记录;

在不更新主键的情况下,如果有任何一个被更新的列更新前更新后占用的存储空间大小不一致,那么就需要把这条旧记录从聚簇索引页面中删掉,然后再根据更新后列的值创建一条新的记录插入到页面。

值得注意的是,这里的删除并不是delete mark而是真正的删除掉,也就是把这条记录从正常记录链移除并加入垃圾链表中,并且修改页面中相应的统计信息,不过这里做真正删除操作的线程并不是purge阶段操作时使用的另外专门的线程,而是由用户线程同步执行真正的删除操作,真正删除之后紧接着就要根据各个列更新后的值创建新记录插入。

如果新创建的记录占用的存储空间大小不超过旧记录占用的空间,那么可以直接重用被加入到垃圾链表中的旧记录所占用的存储空间,否则需要在页面中新申请一段空间以供新记录使用,如果本页面内已经没有可用空间的话,那就需要进行页面分裂操作,然后插入新记录。

针对这种情况设计了一种类型为TRX_UNDO_UPD_EXIST_REC的undo日志。

mysql中undo log日志在哪里 mysql undo文件_mysql中undo log日志在哪里_10


大部分属性与TRX_UNDO_DEL_REC类型的undo日志是类似的:

  • n_updated属性表示本条update语句执行后将有几个列被更新,后边跟着<pos,old_len,old_value>分别表示被更新列在记录中的位置、更新前该列占用的存储空间大小、更新前该列的真实值。
  • 如果update语句中更新的列包含索引列,那么会添加索引列各列信息这个部分,否则不添加。
更新主键的情况

在聚簇索引中,记录是按照主键值的大小连成一个单向链表,如果我们更新了某条记录的主键值,意味着这条记录在聚簇索引中的位置将会发生改变。

针对update语句中更新了记录主键值这种情况,InnoDB在聚簇索引中分了两步处理:

  • 将旧记录进行delete mark操作,这里是delete mark操作,在事务提交之前对旧记录只做一个delete mark操作,事务提交之后专门的线程做purge操作。
  • 根据更新后各列的值创建一条新记录,并将其插入到聚簇索引中,由于更新后的记录主键值发生改变,所以需要重新从聚簇索引中定位这条记录所在的位置,然后把它插进去。