欢迎您光临本站,如有问题请及时联系我们。

《MySQL运维内参》节选 | InnoDB日志管理机制(四)

  什么是MTR InnoDB物理事务

  上面已经提到了关于MTR的概念,实际上,它是InnoDB存储引擎中一个很重要的用来保证物理页面写入操作完整性及持久性的机制。之所以被称为MTR,是因为它的意义相当于一个Mini-transaction,用MTR来表示,这里把它称作“物理事务”,这样叫是相对逻辑事务而言的。

  对于逻辑事务,熟悉数据库的人都很清楚,它是数据库区别于文件系统最重要的特性之一,它具有ACID四个特性,用来保证数据库的完整性——要么都做修改,要么什么都不做。物理事务从名字来看,是物理的,因为在InnoDB存储引擎中,只要是涉及文件修改、文件读取等物理操作的,都离不开这个物理事务,可以说物理事务是Buffer Pool中的内存Page与文件之间的一个桥梁。

  通过下面的图,可以先了解一下MTR在InnoDB中的作用或意义。

MTR

  从上图中可以看出,不管读还是写,只要使用到底层Buffer Pool的页面,都会使用到MTR,它是上面逻辑层与下面物理层的交互窗口,同时也是用来保证下层物理数据正确性、完整性及持久性的机制。

  前面已经介绍过InnoDB的页面Buffer Pool系统,已经知道在访问一个文件页面的时候,系统都会将要访问的页面载入到Buffer Pool中,然后才可以访问这个页面,此时可以读取或更新这个页面。在这个页面不断更新变化的过程中,有一个系统一直扮演着很重要的角色,那就是日志系统。因为InnoDB采用的也是LOGWRITE-AHEAD,所以所有的写操作,都会有日志记录,这样才能保证数据库事务的ACID特性。

  而写日志是一个物理操作,其实它也需要一个完整性。比如在底层页面插入一条记录,如果只修改页头信息而没有修改页尾信息,其实对于这个页面来说是不完整的,所以这个物理操作还是需要一个机制来保证它的完整性的。那么在InnoDB中,这个机制就是上面介绍的物理事务,因为它也是用来保证完整性的,所以也被称作“事务”。

  物理事务既然被称为事务,那它同样有事务的开始与提交,物理事务的开始其实就是对物理事务结构体mtr_struct的初始化,其中包括下面一些成员。

mtr_struct

  分别介绍一下每个成员的意义,如下。

  memo:

  是一个动态数组空间,用来存储所有这个物理事务用到(访问)的页面(实际上存储的就是Buffer Pool中管理页面的控制块结构buf_block_t),这些页面都是被所属的物理事务上了锁的(读锁或者写锁,某些时候会不上锁)。这个锁是读写锁、页面锁,与逻辑事务中所说的表锁和行锁要区分开来。

  log:

  也是一个动态数组空间,用来存储这个物理事务在访问修改数据页面的过程中产生的所有日志,这个日志就是数据库中经常说到的重做(REDO)日志。

  n_log_recs: 表示这个物理事务产生的日志量,单位为日志记录条数。

  log_mode:

  表示这个物理事务的日志模式,包括MTR_LOG_ALL(写日志)、MTR_LOG_NONE(不写日志)等。

  start_lsn: 表示这个物理事务开始前的LSN。

  end_lsn: 表示这个物理事务提交后产生的新的LSN。

  首先,在系统将一个页面载入Buffer Pool的时候,需要一个新开始(mtr_start)或者一个已经开始的物理事务,载入时需要指定页面的获取方式,比如是用来读取的还是用来修改的,这样会影响物理事务对这个页面的上锁情况,如果用来修改,则上X锁,否则上S锁(当然还可以指定不上锁)。在确定了获取方式、页面的表空间ID及页面号之后,就可以通过函数buf_page_get来获取指定页面了,当找到相应页面后,物理事务就要对它上指定的锁,此时需要对这个页面的上锁情况进行检查,一个页面的上锁情况是在结构体buf_block_struct中的lock中体现的,此时如果这个页面还没有上锁,这个物理事务就会直接对其上锁,否则还需要考虑两个锁的兼容性,只有两个锁都是共享锁(S)的情况下才可以上锁成功,否则需要等待。当上锁成功后,物理事务会将这个页面的内存结构存储到上面提到的memo动态数组中,然后这个物理事务就可以访问这个页面了。

  物理事务对页面的访问包括两种操作,一种是读,另一种是写。读就是简单读取其指定页面内偏移及长度的数据;写则是指定从某一偏移开始写入指定长度的新数据,同时如果这个物理事务是写日志的(MTR_LOG_ALL),此时还需要对刚才的写操作记下日志,这里的日志就是逻辑事务中提到的REDO日志。写下相应的日志之后,同样将其存储到上面的log动态数组中,同时要将上面结构体中的n_log_recs自增,维护这个物理事务的日志计数值。

  物理事务的读写过程主要就是上面介绍的内容,其最重要的是它的提交过程。物理事务的提交是通过mtr_commit来实现的。在讲mtr_commit之前,先讲一下,什么时候该提交,内部是如何控制的。

  这里首先需要知道的是,InnoDB的REDO日志不完全是物理日志,它包含了部分逻辑意义在里面,比如插入一行记录时,MTR记录的是在一个页面中写入这条记录,内容大致包括页面号、文件号(表空间号)及这条记录的值(包括每个列信息),这样就有了逻辑概念。需要注意的是,在做REDO恢复时,需要保证这个页面是正确的、完整的,不然这个REDO就会失败,这也正是InnoDB存储引擎中著名的DOUBLEWRITE存在的意义,不过这是后话。而如果是纯物理的REDO,日志内容应该会拆得更散,比如还是插入一条记录,它会记录页面号、文件号(表空间号)、页面内偏移值,并且有多个这样的REDO记录,因为会涉及多个位置的修改操作,这就没有任何逻辑内容了。而针对一个插入操作,需要在一个页面内的不同位置写入不同的数据,当然如果是纯物理REDO,相应地会产生多条REDO记录,这是物理与逻辑的简单区别。

  再说MTR的提交,一个逻辑事务是由多个物理事务组成,逻辑事务是用来保证数据库的ACID特性的,有这个就够了,所以物理事务可以保证一次物理修改是完整的即可。所谓一次物理修改,可以理解为一个底层的相对完整的写入操作,比如插入一条记录的过程中,会包括写一条回滚记录及插入时写入一个页面等,那么这些逻辑上是一个动作的物理写入,就可以被认为是一个独立的物理事务,也就是在写回滚记录时执行mtr_start,写完之后执行mtr_commit,真正插入时写一个页面也是同样的道理。


  (文字太多了,插个图,不要太火啊!)

  接着介绍MTR提交的细节。物理事务的提交主要是将所有这个物理事务产生的日志写入到InnoDB的日志系统的日志缓冲区中,然后等待srv_master_thread线程定时将日志系统的日志缓冲区中的日志数据刷到日志文件中,这会涉及日志刷盘时机的问题,不过还是先来看看MTR、日志缓冲区及日志文件之间的关系,如下图所示。

  从上图中可以看出,左边的若干个MTR产生了各自的REDO LOG,有些MTR已经提交了,有些正在写入。正在写入日志的MTR,它们的日志都存储在自己MTR结构的log动态数组中,这个MTR还是不完整的,所以还是自己保存着,而对于那些已经提交的MTR,它们对应的日志已经在提交的时候转存到了日志缓冲区中,相当于这些日志已经是实实在在地产生了,将来必然要占用数据库日志文件的一部分空间(除非数据库此时挂了)。

  日志缓冲区的存储只是一个暂时的中间状态,日志缓冲区的大小可以通过参数innodb_log_buffer_size来设置,一般都比较小,存储不了太多的日志。因为已经提交并写入到日志缓冲的日志是确定的,所以它们是占用了LSN的,也就是说它们会使LSN变大。

  最后提交的那个MTR代表着整个数据库最新的LSN值,也就是图中所示的

  Log Sequence

  number

  ,这个也正是在MySQL客户端中执行命令

  show engine innodb

  status\G

  时,返回的信息中Log模块中的第一行,如图所示。


  而日志缓冲区也是有大小的,当多个MTR提交时,缓冲区被占满了,那么此时系统会将日志缓冲区的日志刷到日志文件中(这里涉及的另一个问题就是日志刷盘时机,这里只是一种情况,其他的后面做专门介绍),为其他新的MTR释放空间。此时,日志的流向就是从中间的日志缓冲区向右边的日志文件转移,上面已经提到过,转移其实是平移,在缓冲区是什么内容,写入文件也是什么内容,也是完全连续的,且在日志文件中,还是一个个的MTR连续存储。

  最新写入日志文件的那个MTR产生的LSN值(图中所示的log flushed up

  to),其实就是上图中展示的Log状态的第二行,也就是日志最新的写入文件的LSN值,这个值的意义很重大,表示的是,到这个LSN为止,所有的修改都是完整的了,如果此时数据库挂了,写到这个位置的数据都是可以恢复的,而不需要去关心Buffer页面是不是被刷到磁盘。但此时在日志缓冲区中的日志所对应的操作就丢失了,这里是否会丢失事务数据与参数innodb_flush_log_at_trx_commit有关系,如果将参数innodb_flush_log_at_trx_commit设置为1,当前事务的提交肯定会将日志缓冲区中的日志刷到日志文件中;如果设置为2,那么日志只是写入了操作系统缓存,并没有写入磁盘,那么此时有可能丢失部分已经提交的事务,丢失多少由操作系统决定,这种情况下,即使数据库挂了,只要机器不挂,就问题不大,因为操作系统还会将它对应的缓存写入磁盘;但如果设置为0的话,就无能为力了,因为InnoDB只负责将事务对应的日志写入到日志缓冲区中,无论是操作系统,还是数据库,都不能保证日志的安全性,所以最好不要设置成这样。

  进一步而言,日志文件的大小也是有限的,不可能无限量地将日志写入日志文件中。前面已经提到过,它是循环使用的,如果日志写入的头(图中所示的log

  flushed up to)和尾相遇了,此时日志就不能再写入了,因为如果再写入的话,就要“追尾”了,这样会将之前产生的日志覆盖掉,导致日志不可用,不完整。此时就会使用一种机制来保证新的日志还能继续写入,尾部日志还是完整的,这个机制叫做checkpoint(检查点)。

  说白了,日志产生的作用,是将随机页面的写入变成顺序日志的写入,从而用一个速度更快的写入来保证速度较慢的写入的完整性,从而提高整体数据库的性能。其根本目的是要将随机变成顺序,所以日志的量才是一个相对固定循环使用的空间。有了这个思想之后,使用检查点来保证日志的重复写入、数据库完整性就是顺其自然的事情。

 (年青俊彦单身中,欢迎留言咨询)

  使用检查点来保证数据库完整性的主体思想,主要是让日志失效,也就是让Buffer Pool中的页面修改写入到磁盘上面。因为日志的存在实际上就是让Buffer

  Pool中的Page尽可能少地刷磁盘,尽可能长时间地将页面数据缓存起来,尽可能提高访问速度,因为不管如何修改,Buffer Pool中的页面都是最新的,只是不一定写入磁盘中(没有刷入没关系,由日志来保证)。如果日志文件大小不够用,此时只要将Buffer Pool中的某些页面刷入到磁盘中,其对应的日志就失效了,因为这些日志就是用来保证Page没有刷入时但数据库挂了的情况下数据库的完整性的,而这些Page如果已经写入磁盘了,相应的日志也就没有用了,这就是检查点的根本意义所在。

  而上面提到的,做检查点时,只是将某些页面刷入磁盘,其中的”某些”是有讲究的。俗话说:“家有三件事,先从紧处来”,现在的问题是日志空间不够用了,而日志是循环使用的,必须是按照顺序,不能跳着写,所以最主要的是从LSN值最小的日志开始,按照从小到大的顺序不断地让这些日志失效。每次做检查点都会有一个比例,此时系统会根据最小的有效LSN(min_valid_lsn)和检查点处理的日志比例计算出最大的将要失效的LSN值(取名叫lsn_checkpoint_up_to)。计算完之后,再去扫描Buffer Pool的flush_list链表,找出所有被更新过的页面中,曾经修改这些页面的MTR对应的LSN中的最小值(因为一个页面有可能被多次修改,但只需要考虑最小的LSN的那一次,使用的是前面介绍结构buf_block_t时,这里面所存储的oldest_modification的值),如果这个值比lsn_checkpoint_up_to值小,就将这个页面刷入磁盘,也就是说,如果将小于lsn_checkpoint_up_to的MTR修改过的页面都刷入磁盘了,那么日志文件中在LSN值lsn_checkpoint_up_to以前的日志就都可以失效了,那么在整个日志文件空间中,从min_valid_lsn到lsn_checkpoint_up_to之间的空间,又可以被重新使用了,直接覆盖即可,而不会导致数据库不完整、数据丢失等问题。

  上面讲的整个检查点过程,用一个更形象的图表示如下。


  此时,再接着上面MTR产生日志的图来讲,上面找到的日志文件的位置lsn_checkpoint_up_to就是图中所示的

  last checkpoint at

  ,也是上面命令

  show engine innodb status\G

  中关于Log部分的第四行信息。而从这个点开始到最新的已经刷盘的日志文件位置

  Log flushed up to

  之间的日志都是有效日志了,不能被覆盖,只有空间又不够用了的情况下,再将最小的有效日志位置向前推,产生新的位置,像这样不断循环,周而复始的工作,这就是日志、Buffer Pool及检查点之间的工作原理。

  上面提到的

  show engine innodb status\G

  命令生成的Log部分中显示的第三行信息,是Buffer Pool中Page刷盘时刷到的一个最新的LSN。但此时检查点的最新点不一定做得及时,所以它是大于等于第四行的,而图中所示的四行对应的值,从上到下以递减的顺序排序,其中的道理都已经非常明确了。

  上面已经讲过,物理事务和逻辑事务一样,也是可以保证数据库操作的完整性的。一般说来,一个操作必须要在一个物理事务中完成,也就是说要么这个操作已经完成,要么什么也没有做,否则就有可能造成数据不完整的问题,因为在数据库系统做REDO操作时是以一个物理事务为单位做的,如果一个物理事务的日志是不完整的,则它对应的所有日志都不会重做。那么,如何辨别一个物理事务是否完整呢?这个问题是在物理事务提交时用了一个很巧妙的方法来保证的。在提交前,如果发现这个物理事务有日志,则在日志最后再写一些特殊的日志,这些特殊的日志就是一个物理事务结束的标志,提交时一起将这些特殊的日志写入,在重做时如果当前这一批日志信息最后面存在这个标志,则说明这些日志是完整的,否则就是不完整的,就不会重做。

  物理事务提交时还有一项很重要的工作就是处理上面结构体中动态数组memo中的内容,现在已经知道这个数组中存储的是这个物理事务访问过的所有页面,并且都已经上了锁。在它提交时,如果发现这些页面中已经有被修改过的,这些页面就成了脏页,这些脏页需要被加入到InnoDB Buffer Pool中的更新链表中(讲BUFFER时已经讲过);当然,如果已经在更新链中,则直接跳过(不能重复加入),svr_master_thread线程会定时检查这个链表,将一定数目的脏页刷到磁盘中,加入之后还需要将这个页面上的锁释放掉,表示这个页面已经处理完成;如果页面没有被修改,或者只是用来读取数据的,则只需要直接将其共享锁(S锁)释放掉即可。

  上面的内容就是物理事务的一个完整的讲述,它是比较底层的一个模块,牵扯的东西比较多,这里重点讲述了物理事务的意义、操作原理、与BUFFER系统的关联、日志的产生等内容。


来源:本文由E8运维原创撰写,欢迎分享本文,转载请保留出处和链接!