厂商动态 > 正文

全方位解读 MySQL 日志实现内幕(五)

2020-02-14 11:20:35来源:

原标题:全方位解读MySQL日志实现内幕(五)

作者介绍

------

本文作者将出版于《MySQL运维内参》中部分内容进行分享,通过多篇文章连载形式,全方位介绍MySQL日志实现内幕,本篇为这部分最后一篇,想了解更多可查看前面四篇内容!

数据库回滚

在第5章讲述InnoDB存储引擎启动的那一节中,已经知道,在调用了函数recv_recovery_from_checkpoint_start之后,又调用了recv_recovery_from_checkpoint_finish。这里就是用来做数据库回滚的地方,也可以看出来,InnoDB的REDO是在UNDO之前做的,是等到物理的数据库操作都完成之后,才能在物理数据一致的基础上去做一些逻辑的操作,即UNDO回滚操作。

在讲如何回滚之前,需要先了解UNDO日志的存储方式。

数据库UNDO段管理

回滚段的管理,也是有一个入口位置用来存储回滚段的管理信息。在InnoDB中,是用第6个页面(5号)来管理的,这个页面是专门用来存储事务相关信息的,先来看一下其页面格式,如下。

  1. /**Transactionsystemheader*/
  2. /*-------------------------------------------------------------@{*/
  3. #defineTRX_SYS_TRX_ID_STORE0/*!<themaximumtrxidortrx
  4. numbermodulo
  5. TRX_SYS_TRX_ID_UPDATE_MARGIN
  6. writtentoafilepagebyany
  7. transaction;theassignmentof
  8. transactionidscontinuesfrom
  9. thisnumberroundedupby
  10. TRX_SYS_TRX_ID_UPDATE_MARGIN
  11. plus
  12. TRX_SYS_TRX_ID_UPDATE_MARGIN
  13. whenthedatabaseis
  14. started*/
  15. #defineTRX_SYS_FSEG_HEADER8/*!<segmentheaderforthe
  16. tablespacesegmentthetrx
  17. systemiscreatedinto*/
  18. #defineTRX_SYS_RSEGS(8+FSEG_HEADER_SIZE)
  19. /*!<thestartofthearrayof
  20. rollbacksegmentspecification
  21. slots*/

面定义的是第6号页面中存储的信息及其对应的位置,每一项的详细意义如下。

  • TRX_SYS_TRX_ID_STORE:用来存储事务号,在每次新启动一个事务时,都会去检查当前最大事务号是不是达到了TRX_SYS_TRX_ID_WRITE_MARGIN(256)的倍数,如果达到了,就会将最大的事务号写入这个位置,在下次启动时,将这个值取出来,再加上一个步长(256),来保证事务号的唯一性,其实就是一个经典取号器的实现原理。
  • TRX_SYS_FSEG_HEADER:用来存储事务段信息。
  • TRX_SYS_RSEGS:这是一个数组,InnoDB有128个回滚段,那这个数组的长度就是128,每一个元素占用8个字节,对应的一个回滚段存储的内容包括回滚段首页面的表空间ID号及页面号。

而针对每一个回滚段,即上面数组中的一个元素,也有其自己的存储格式,代码中的宏定义如下。

  1. #defineTRX_RSEG_MAX_SIZE0/*Maximumallowedsizeforrollback
  2. segmentinpages*/
  3. #defineTRX_RSEG_HISTORY_SIZE4/*Numberoffilepagesoccupied
  4. bythelogsinthehistorylist*/
  5. #defineTRX_RSEG_HISTORY8/*Theupdateundologsforcommitted
  6. transactions*/
  7. #defineTRX_RSEG_FSEG_HEADER(8+FLST_BASE_NODE_SIZE)
  8. /*Headerforthefilesegmentwhere
  9. thispageisplaced*/
  10. #defineTRX_RSEG_UNDO_SLOTS(8+FLST_BASE_NODE_SIZE+FSEG_HEADER_SIZE)
  11. /*Undologsegmentslots*/

上面这些信息的存储,是从页面偏移38的位置开始的,在这个位置之前,存储的是文件管理信息(请参考第8章),从38开始,存储了上面五个信息,它们的意义分别如下。

  • TRX_RSEG_MAX_SIZE:回滚段管理页面的总数量,即所有undo段页面之和,一般为ULINT_MAX,即无上限。
  • TRX_RSEG_HISTORY_SIZE:这个表用来表示当前InnoDB里,在HistoryList中有多少个页面,即需要做PURGE的回滚段页面的个数。
  • TRX_RSEG_HISTORY:用来存储HistoryList的链表首地址,事务提交之后,其对应的回滚段如果还不能PURGE,就都会加入到这个链表中。
  • TRX_RSEG_FSEG_HEADER:用来存储回滚段的Inode位置信息,通过这个地址,就可以找到这个段的详细信息。
  • TRX_RSEG_UNDO_SLOTS:这个位置所存储的是一个数组,长度为1024,每一个元素是一个页面号,初始化为FIL_NULL,即空页面。

这五个信息,存储了一个回滚段的信息,最后一个位置的数组,用来真正存储回滚段的位置,后面会讲到这128*1024个槽是如何使用的。

根据上面的讲述,现在已经知道所有回滚段的存储架构了,如图11.12所示。

在每一个事务开始的时候,都会分配一个rseg,就是从长度为128的数组中,根据最近使用的情况,找到一个临近位置的rseg,在这个事务的生命周期内,被分配的rseg就会被这个事务所使用。

在事务执行的过程中,会产生两种回滚日志,一种是INSERT的UNDO记录,一种是UPDATE的UNDO记录,可能有人会问DELETE哪去了?其实是包含在UPDATE的回滚记录中,因为InnoDB把UNDO分为两类,一类就是新增,也就是INSERT,一类就是修改,就是UPDATE,分类的依据就是事务提交后要不要做PURGE操作,因为INSERT是不需要PURGE的,只要事务提交了,那这个回滚记录就可以丢掉了,而对于更新和删除操作而言,如果事务提交了,还需要为MVCC服务,那就需要将这些日志放到HistoryList中去,等待去做PURGE,以及MVCC的多版本查询等,所以分为两类。

所以,一个事务被分配了一个rseg之后,通常情况下,如果一个事务中既有插入,又有更新(或删除),那么这个事务就会对应两个UNDO段,即在一个rseg的1024个槽中,要使用两个槽来存储这个事务的回滚段,一个是插入段,一个是更新段。

在事务要存储回滚记录的时候,事务就要从1024个槽中,根据相应的更新类型(插入或者更新)找到空闲的槽来作为自己的UNDO段。如果已经申请过相同类型的UNDO段,就直接使用,否则就需要新创建一个段,并将段首页号写入这个rseg长度为1024的数组的对应位置(空闲位置)中去,这样就将具体的回滚段与整个架构联系起来了。

如果在1024个槽中找不到空闲的位置,那么这个事务就会被回滚掉,报出错误:“Toomanyactiveconcurrenttransactions”,错误号为1637的异常。当然,这种情况一般不会见到,如果能把这个用完,估计数据库已经根本动不了了。

上面讲述了整个回滚段存储架构及与事务的相关性,具体到一个事务所使用的某个回滚段的管理,就存储在了回滚段首页中,管理信息包括3部分,分别是Undopageheader、Undosegmentheader及Undologheader。下面来分别介绍。

1.Undopageheader。

  1. /**Transactionundologpageheaderoffsets*/
  2. #defineTRX_UNDO_PAGE_TYPE0/*!<TRX_UNDO_INSERTor
  3. TRX_UNDO_UPDATE*/
  4. #defineTRX_UNDO_PAGE_START2/*!<Byteoffsetwheretheundolog
  5. recordsfortheLATESTtransaction
  6. startonthispage(rememberthat
  7. inanupdateundolog,thefirstpage
  8. cancontainseveralundologs)*/
  9. #defineTRX_UNDO_PAGE_FREE4/*!<Oneachpageoftheundologthis
  10. fieldcontainsthebyteoffsetofthe
  11. firstfreebyteonthepage*/
  12. #defineTRX_UNDO_PAGE_NODE6/*!<Thefilelistnodeinthechain
  13. ofundologpages*/
  • TRX_UNDO_PAGE_TYPE:这个在上面已经解释过了,就包括两个值,分别是TRX_UNDO_INSERT和TRX_UNDO_UPDATE。
  • TRX_UNDO_PAGE_START:用来表示当前页面中,从什么位置开始存储了UNDO日志。
  • TRX_UNDO_PAGE_FREE:与上面的START相对,这个用来表示当前页面中,UNDO日志的结束位置,也表示从这个位置开始,可以继续追加UNDO日志,直到页面存储满为止。
  • TRX_UNDO_PAGE_NODE:一个UNDO段中所有的页面,通过一个双向链表来管理,这个位置存储的就是双向链表的指针。

2.Undosegmentheader。

  1. /**Undologsegmentheader*/
  2. #defineTRX_UNDO_STATE0/*!<TRX_UNDO_ACTIVE,...*/
  3. #defineTRX_UNDO_LAST_LOG2/*!<Offsetofthelastundologheader
  4. onthesegmentheaderpage,0if
  5. none*/
  6. #defineTRX_UNDO_FSEG_HEADER4/*!<Headerforthefilesegmentwhich
  7. theundologsegmentoccupies*/
  8. #defineTRX_UNDO_PAGE_LIST(4+FSEG_HEADER_SIZE)
  9. /*!<Basenodeforthelistofpagesin
  10. theundologsegment;definedonlyon
  11. theundologsegment'sfirstpage*/
  • TRX_UNDO_STATE:用来存储当前UNDO段的状态,状态包括TRX_UNDO_ACTIVE,TRX_UNDO_CACHED、TRX_UNDO_TO_FREE、TRX_UNDO_TO_PURGE、TRX_UNDO_PREPARED五种。
  • TRX_UNDO_LAST_LOG:用来存储最后一个UNDO日志的偏移位置,用来在一个UNDO段中,找到最后一个UNDO日志。
  • TRX_UNDO_FSEG_HEADER:这个位置,就是用来存储当前UNDO段的Inode信息的,通过这个信息可以知道本UNDO段的详细信息。
  • TRX_UNDO_PAGE_LIST:段内所有的页面都是通过链表连接起来的,这个位置是链表的首地址,用来管理这个链表,上面已经介绍的TRX_UNDO_PAGE_NODE则是每个节点的双链指针。

3.Undologheader。

  1. /**Theundologheader.Therecanbeseveralundologheadersonthefirst
  2. pageofanupdateundologsegment.*/
  3. #defineTRX_UNDO_TRX_ID0/*!<Transactionid*/
  4. #defineTRX_UNDO_TRX_NO8/*!<Transactionnumberofthe
  5. transaction;definedonlyifthelog
  6. isinahistorylist*/
  7. #defineTRX_UNDO_DEL_MARKS16/*!<Definedonlyinanupdateundo
  8. log:TRUEifthetransactionmayhave
  9. donedeletemarkingsofrecords,and
  10. thuspurgeisnecessary*/
  11. #defineTRX_UNDO_LOG_START18/*!<Offsetofthefirstundologrecord
  12. ofthislogontheheaderpage;purge
  13. mayremoveundologrecordfromthe
  14. logstart,andthereforethisisnot
  15. necessarilythesameasthislog
  16. headerendoffset*/
  17. #defineTRX_UNDO_XID_EXISTS20/*!<TRUEifundologheaderincludes
  18. X/OpenXAtransactionidentification
  19. XID*/
  20. #defineTRX_UNDO_DICT_TRANS21/*!<TRUEifthetransactionisatable
  21. create,indexcreate,ordrop
  22. transaction:inrecovery
  23. thetransactioncannotberolledback
  24. intheusualway:a'rollback'rather
  25. meansdroppingthecreatedordropped
  26. table,ifitstillexists*/
  27. #defineTRX_UNDO_TABLE_ID22/*!<Idofthetableifthepreceding
  28. fieldisTRUE*/
  29. #defineTRX_UNDO_NEXT_LOG30/*!<Offsetofthenextundologheader
  30. onthispage,0ifnone*/
  31. #defineTRX_UNDO_PREV_LOG32/*!<Offsetofthepreviousundolog
  32. headeronthispage,0ifnone*/
  33. #defineTRX_UNDO_HISTORY_NODE34/*!<Ifthelogisputtothehistory
  34. list,thefilelistnodeishere*/

这是一个针对UNDO日志的头信息,一个事务写入一次UNDO日志就会创建一个UNDO日志单元,都会对应一个这样的UNDO日志头信息,用来管理这个日志信息的状态,存储一些相关的信息以备恢复时使用,多个UNDO日志之间,通过双向链表连接起来(通过即将介绍的TRX_UNDO_NEXT_LOG及TRX_UNDO_PREV_LOG来管理)。

  • TRX_UNDO_TRX_ID:用来存储当前UNDO日志对应事务的事务ID号。
  • TRX_UNDO_TRX_NO:事务序列号,在恢复时使用,这个序列号就是前面讲的TRX_SYS_TRX_ID_STORE位置存储的ID值。这个与上面ID的区别是,NO用来在回滚时保持顺序使用,而ID是在事务运行时使用的。
  • TRX_UNDO_DEL_MARKS:用来表示当前UNDO日志中有没有通过打标志删除过记录的操作,并决定是不是要做PURGE操作。
  • TRX_UNDO_LOG_START:用来存储当前页面中,第一个UNDO日志的开始位置。
  • TRX_UNDO_XID_EXISTS:用来标志当前日志中有没有包含Xid事务。
  • TRX_UNDO_DICT_TRANS:用来标志当前日志对应的事务是不是DDL的,用来在回滚时判断如何操作。
  • TRX_UNDO_TABLE_ID:与上一个相关,如果上面的标志是真的,则这个标志的是DDL的表ID。
  • TRX_UNDO_NEXT_LOG:用来链接当前UNDO段中所有的UNDO日志,这个是指向下一个UNDO日志。
  • TRX_UNDO_PREV_LOG:与上一个对应,这个用来指向上一个UNDO日志,从而构成双向链表。
  • TRX_UNDO_HISTORY_NODE:用来存储在HistoryList中的双向链表指针。而这个链表的首地址,是在之前介绍的TRX_RSEG_HISTORY位置,可以回到前面去查看相关信息。

到目前为止,关于具体一个UNDO段中每个页面及页面内容是如何管理的已经讲清楚了。当一个事务需要写入UNDO日志时,就可以直接从对应的UNDO段中找到一个页面及对应的追加日志的偏移位置,然后将对应的UNDO日志写入即可。

数据库UNDO日志记录格式

在存储已经搞定之后,那么还需要继续研究一个要写入的UNDO日志记录的格式是什么样子的。关于记录格式,之前也介绍过InnoDB表中行记录(Compact)的格式,也介绍了REDO日志的记录格式,其实都是本着省空间、高效率的宗旨来设计的,那么对于UNDO记录也是一样,但是因为UNDO日志有多个类型,针对不同的类型,其格式也不尽相同,UNDO日志的类型有下面四种。

  • TRX_UNDO_INSERT_REC:记录插入的UNDO日志类型,插入记录用于回滚时,只需要通过其主键就可以实现回滚操作,所以在UNDO日志中,只记录了表ID及主键信息。回滚时,只需要通过记录中存储的主键,在原B+树中找到对应的记录,然后将其删除即可。
  • TRX_UNDO_UPD_EXIST_REC:更新一条存在记录的UNDO日志类型。在日志内容中,需要记录的除了表ID信息之外,还需要记录每一个被更新的列的原始值和新值,同时还需要记录主键信息用于回滚时的检索。回滚时,还是根据主键信息,找到对应的记录,然后以旧换新,恢复原值即可。
  • TRX_UNDO_UPD_DEL_REC:更新一条已经打了删除标志记录的UNDO日志类型。格式与上面是一样的,回滚方法也同上。
  • TRX_UNDO_DEL_MARK_REC:删除记录时对记录打删除标志的UNDO日志类型,格式与上面插入操作的UNDO日志格式一样,只需要存储主键信息和表ID信息,用来在回滚或者PURGE时找到对应的记录即可。回滚时,根据主键信息,找到对应的记录,然后将删除标志去掉即完成回滚。

除了上面说到的TableID信息、主键信息之外,还会包括一些公有的信息,比如回滚段指针、最近更新事务号,这样方便MVCC在回溯记录时可以找到以前的版本,关于MVCC的内容在这里就不详细展开了。

再回到记录格式。因为记录格式都不尽相同,所以这里只拿TRX_UNDO_INSERT_REC来举例说明,图11.13即为其格式。

每一个位置的解释如下。

  • 可以看到在整个记录最前面的两个字节和最后面的两个字节是用来方便找到每一个记录的,并且通过这两条信息,就可以找到每一个UNDO页面中的所有记录,相当于是一个由UNDO记录组成的双向链表,因为对于UNDO记录,回滚过程是一个反向操作的过程,所以需要从后向前的搜索功能。
  • 第二个位置存储的就是UNDO记录类型。
  • 第三个位置存储的是一个事务的undo_no,用来区分一个事务中的多个UNDO日志的顺序。
  • 接下来的位置用来存储当前回滚记录对应的表ID,接下来的trx_id存储的就是更新这条记录时的事务ID,即当前事务的事务ID。
  • 再接着,roll_ptr用来存储当前被更新记录的上一个版本在回滚段中的位置,即这条记录中隐式列roll_ptr的值(用来在读取数据时可以找到老的版本),而当前记录的这个列的值,在写完这条UNDO日志之后,即将被修改为当前UNDO日志的位置,从而实现了一个隐式的单链表,可以使用roll_ptr的值一直回溯到第一次更新之前的版本。
  • 再接下来的位置,存储的就是真实的主键信息了,存储格式是用前面若干个字节存储列数据的长度,而后面接着其数据,这样依次将所有的主键列存储完。
  • 最后的位置,在第一点中已经介绍过了。

从图11.13中可以看到,很多位置的存储都是压缩存储的,所以上面第六点说到,列数据长度用的字节个数有可能是若干个,这决定于InnoDB所使用的压缩编码方式。

这里需要注意的一点是,与REDO日志记录存储不同,UNDO日志的存储,是不会跨页面的,所以在页面头中关于日志存储的开始位置和结束位置就至关重要了。

其他类型的回滚记录,这里就不再介绍了,大致结构是一样的,只不过内容可能不尽相同。

需要注意的一点是,假如一个表中有多个索引,在修改一行数据时,回滚日志中也只会记录聚簇索引中的信息,而其他二级索引是不会被记录的。这是因为聚簇索引和二级索引中的每一行都是一一对应的,所以不同操作对聚簇索引操作时,也都会对二级索引有相应的操作,这样就没必要对二级索引写回滚日志了。

回滚时刻

前面已经介绍过,UNDO日志的正确性是通过REDO的恢复来保证的,在REDO日志恢复完成之后,UNDO操作就可以安全地进行了。数据库启动过程中,执行了用于REDO恢复的函数recv_recovery_from_checkpoint_start之后,就可以处理UNDO的数据了,InnoDB通过函数trx_sys_init_at_db_start来将所有回滚段相关的128*1024个UNDO扫描出来(如果存在就找到,不存在就忽略),找到之后,每一个UNDO段的状态都已经清楚了,然后将它们都缓存起来。

然后再通过函数trx_lists_init_at_db_start依次处理每一个UNDO段,根据UNDO段的状态,决定后面将采取什么措施,如果状态为TRX_UNDO_PREPARED和TRX_UNDO_ACTIVE,则这个UNDO段是需要做回滚操作的,否则是不需要的。决定回滚需求之后,再将最多128*1024个UNDO段按照上面提到的TRX_UNDO_TRX_NO从大到小的顺序排序。

最后就在之前介绍关于InnoDB存储引擎启动时的函数recv_recovery_from_checkpoint_finish中,来做回滚的相关工作。在这个函数的最后可以看到以下内容。

  1. /*Rollbackanyrecovereddatadictionarytransactions,so
  2. thatthedatadictionarytableswillbefreeofanylocks.
  3. Thedatadictionarylatchshouldguaranteethatthereisat
  4. mostonedatadictionarytransactionactiveatatime.*/
  5. if(srv_force_recovery<SRV_FORCE_NO_TRX_UNDO){
  6. trx_rollback_or_clean_recovered(FALSE);
  7. }

它根据参数innodb_force_recovery来决定要不要做回滚操作,如果设置为3或3以上,就不回滚了,这样可能导致数据库逻辑上的不一致。

最终,InnoDB通过trx_rollback_or_clean_recovered来做回滚操作,通过扫描上面排序之后的链表,发现其还是以从大到小的顺序遍历,这个顺序很重要,因为UNDO是反向操作,所以应该是先处理新产生的事务,后处理老的事务,通过事务号来区分新老关系。

针对每一个UNDO段,InnoDB会将所有状态为ACTIVE的事务的UNDO日志扫描出来,然后一条一条地做回滚操作,UNDO日志记录格式已经明确,扫描所有的日志就变得非常简单,并且针对不同的操作,对应的回滚方式也已经清楚,等待所有的回滚段处理完成之后,整个数据库的回滚操作也就完成了。回滚过程如图11.14所示。

到这里,InnoDB就可以继续启动了,此时的数据库处于一个完整的、可以正确提供线上服务的状态。

总结

关于日志的实现,是一个自成一套的理论体系。开头已经讲过,在几十年前就有了这方面的相关论文,并且目前基本所有大型数据库都是以这个为理论基础的,InnoDB也不例外。

日志在数据库实现中所涉及的内容非常多,本章只能尽可能地将它们连贯起来,搞清楚它们之间的关系,以保证在日常学习及运维中,做到相互联系、辩证地看问题,以看到问题的本质。

本章提到的每一部分都可以自成一章,但这不是本书的初衷,能够让对MySQL(InnoDB)感兴趣的同学能将所有这些模块连贯起来,融会贯通,做到对其尽可能多地理解就够了。

来源:搜狐

  • 电影花絮
  • 电影情报
  • 圈子新闻
  • 电影新闻
  • 电影搜罗
  • 电视剧
  • 影视演员
推荐阅读