引子:
书接上文,在之前六篇讲述了写日志,其实正常情况下,这都是无用功,因为根本用不到。上一节讲到了,在什么情况下会用到日志,以及在什么时候会用到,如何用到等等内容,我们这一节继续讲述,在扫描完成日志之后,如何做数据库恢复工作,里面有什么逻辑,有什么可以改进的地方等等,这都是我们读者要去深思的地方。
(本书作者在“白家大院”齐聚首)
从这些代码段中可以看到,缓存到HASH表之后,应该是可以找合适的时机去APPLY了,那什么时候呢?我们可以返回去看看函数recv_scan_log_recs的最后,调用了函数recv_apply_hashed_log_recs,那这个就是我们要找的真正做APPLY的函数了。我们详细看一下它的实现。
继续:
从这些代码段中可以看到,缓存到HASH表之后,应该是可以找合适的时机去APPLY了,那什么时候呢?我们可以返回去看看函数recv_scan_log_recs的最后,调用了函数recv_apply_hashed_log_recs,那这个就是我们要找的真正做APPLY的函数了。我们详细看一下它的实现。
UNIV_INTERN void recv_apply_hashed_log_recs(
ibool allow_ibuf
)
{
/* local vaiables … */
loop:
recv_sys->apply_log_recs = TRUE;
recv_sys->apply_batch_on = TRUE;
/* 遍历HASH表?是的,把HASH表中的每一个桶中的每一个页面,连续处理 */
for (i = 0; i < hash_get_n_cells(recv_sys->addr_hash); i++) {
/* 遍历HASH表一个桶中的多个地址 */
for (recv_addr = static_cast<recv_addr_t*>(
HASH_GET_FIRST(recv_sys->addr_hash, i));
recv_addr != 0;
recv_addr = static_cast<recv_addr_t*>(
HASH_GET_NEXT(addr_hash, recv_addr))) {
/* 针对每一个页面,做这个页面上所有的REDO操作 */
ulint space = recv_addr->space;
ulint zip_size = fil_space_get_zip_size(space);
ulint page_no = recv_addr->page_no;
if (recv_addr->state == RECV_NOT_PROCESSED) {
mutex_exit(&(recv_sys->mutex));
if (buf_page_peek(space, page_no)) {
buf_block_t* block;
mtr_start(&mtr);
block = buf_page_get(
space, zip_size, page_no,
RW_X_LATCH, &mtr);
buf_block_dbg_add_level(
block, SYNC_NO_ORDER_CHECK);
/* 恢复一个页面的数据,APPLY recv_addr中存储的所有REDO记录,
这里使用了一个MTR来恢复。需要注意的是,这个MTR只是用来
获取页面时,给这个页面加锁使用的,而不会涉及REDO操作,因为
REDO是不需要再写日志的,所以不用担心这个MTR涉及到的日志量
太大的问题 */
recv_recover_page(FALSE, block);
mtr_commit(&mtr);
} else {
/* 这里的操作是,如果上面的buf_page_peek没有在Buffer Pool中
找到这个页面,那这里就从文件中将这个页面载入到Buffer Pool,
并且预读32个页面以提高性能。恢复方法与是一样的。*/
recv_read_in_area(space, zip_size, page_no);
}
mutex_enter(&(recv_sys->mutex));
}
}
}
/* Wait until all the pages have been processed */
while (recv_sys->n_addrs != 0) {
mutex_exit(&(recv_sys->mutex));
os_thread_sleep(500000);
mutex_enter(&(recv_sys->mutex));
}
/* Wait for any currently run batch to end.
如注释所述,如果上面的操作做完了,则需要保证这些日志APPLY之后
要在ibdata及ibd(s)中落地,此时就会将Buffer Pool中全部的脏页刷一遍
以保证已经处理的这些日志失效。可能有人会问,如果在恢复的过程中,假设
就是这里吧,还没有做刷盘操作,数据库又挂了,那怎么办?
其实没事儿,整个恢复过程,日志也没有写,只是扫描了一遍,并且有可能在
Buffer Pool中已经写了很多页面,有可能这些页面已经因为LRU已经刷过
了,但这些操作是可重入的,也就是说,数据库再起来,可以重新做一次REDO
操作,直到做成功为止。*/
success = buf_flush_list(ULINT_MAX, LSN_MAX, NULL);
recv_sys->apply_log_recs = FALSE;
recv_sys->apply_batch_on = FALSE;
/* 将HASH表中缓存的所有内容清空 */
recv_sys_empty_hash();
mutex_exit(&(recv_sys->mutex));
}
到这里,我们应该已经清楚了REDO数据库恢复的整个过程,并且可以返回到函数recv_recovery_from_checkpoint_start_func中,看一下最后的说明,做完REOD之后,做一次检查点以说明这次数据库恢复已经完成。
但这里我又有话说了,各位同学有没有发现一个细节,那就是InnoDB在辛辛苦苦的将所有日志分析并且根据不同页面通过HASH表存储之后,我们特别要注意下面两点特征:
- 对于同一个页面的REDO记录,必然是存储在同一个HASH桶中的。
- 对于某一个页面的所有日志记录,是按照先后顺序来管理的。
这两个特征非常重要,因为我们知道,REDO日志的APPLY,与顺序有关系,LSN小的,必定要比LSN大的先做APPLY,不然有可能造成数据的覆盖。但这有一个前提就是同一个页面,不同页面之间是不存在这样的问题的。
那我们想想,是不是只需要保证,同一个页面的日志顺序执行其所有的日志记录即可,而不同页面就没必要守这个规则了,答案是肯定的。
那目前InnoDB难道不是这样做的么?上面代码中我们已经看到了,他是用了一个两层循环,扫描了整个HASH表,慢慢的一条条的做REDO恢复。基于上面的分析,其实可以大胆的想象的一下,REDO恢复可以实现并行恢复。按照桶的下标为键值分配线程,那这样同一个桶必然会分到同一个线程中去做,这样自然保证了同一个页面的执行顺序,而不同的桶之间的页面是没有关系的,自然就可以并行恢复了。
啊?可以这样?这个想法,可能会让那些把日志文件设置的很大,又经常出现机器宕机问题的同学(上面已经提到了他们)心潮澎湃,这样性能提升的不只一点点了。
还是那句话,这个是需要把日志文件设置很大,并且经常出现宕机时,优化效果才明显。有需求,就能解决,我们希望这个优化会出现在某个版本中,少一些浪费的时间。
到现在为止,REDO日志的恢复就做完了,到这个时候,才真正体现了这个“累赘”的价值,感谢有你!
上面所讲的,是使用REDO日志来恢复数据库的过程,在它做完之后,整个数据库就是完整的了,已经保证了所有的数据库表都没有丢数据的情况,所有的数据库页面也已经是完整的了。假设此时对数据库做DML操作,也已经是可以的了,但还有一个问题没有处理,那就是此时的数据库,存在脏数据。因为有些事务没有提交,但数据已经存在了(举一个例子,事务在做的过程中,日志已经写完并刷盘,就是没有提交,此时数据库挂了),那根据事务的ACID特性,这样的数据就不应该存在,此时InnoDB需要做的就是把这些事务回滚掉,这就用到了我们下面将要讲的“数据库回滚”。
(神形兼备啊,另外那种霸气也流露出来了)
数据库回滚
回滚段的管理,也是有一个入口位置用来存储回滚段的管理信息的,在InnoDB中,是用第6个页面(5号)来管理的,这个页面是专门用来存储事务相关的信息的,我们先看看其页面格式:
/** Transaction system header */
/*————————————————————- @{ */
#define TRX_SYS_TRX_ID_STORE 0 /*!< the maximum trx id or trx
number modulo
TRX_SYS_TRX_ID_UPDATE_MARGIN
written to a file page by any
transaction; the assignment of
transaction ids continues from
this number rounded up by
TRX_SYS_TRX_ID_UPDATE_MARGIN
plus
TRX_SYS_TRX_ID_UPDATE_MARGIN
when the database is
started */
#define TRX_SYS_FSEG_HEADER 8 /*!< segment header for the
tablespace segment the trx
system is created into */
#define TRX_SYS_RSEGS (8 + FSEG_HEADER_SIZE)
/*!< the start of the array of
rollback segment specification
slots */
上面定义的是第6号页面中存储的信息及其对应的位置,每一项的详细意义如下:
- TRX_SYS_TRX_ID_STORE:用来存储事务号,在每次新启动一个事务时,都会去检查当前最大事务号是不是达到了TRX_SYS_TRX_ID_WRITE_MARGIN(256)的倍数,如果达到了,就会将最大的事务号写入到这个位置,在下次启动时,将这个值取出来,再加上一个步长(TRX_SYS_TRX_ID_WRITE_MARGIN),来保证事务号的唯一性,其实就是一个经典的取号器的实现原理。
- TRX_SYS_FSEG_HEADER:用来存储事务段信息。
- TRX_SYS_RSEGS:这是一个数组,InnoDB有128个回滚段,那这个数组的长度就是128,每一个元素占用8个字节,对应的一个回滚段存储的内容包括回滚段首页面的表空间ID号及页面号。
而针对每一个回滚段,即上面数组中的一个元素,也有其自己的存储格式,代码中的宏定义如下:
#define TRX_RSEG_MAX_SIZE 0 /* Maximum allowed size for rollback
segment in pages */
#define TRX_RSEG_HISTORY_SIZE 4 /* Number of file pages occupied
by the logs in the history list */
#define TRX_RSEG_HISTORY 8 /* The update undo logs for committed
transactions */
#define TRX_RSEG_FSEG_HEADER (8 + FLST_BASE_NODE_SIZE)
/* Header for the file segment where
this page is placed */
#define TRX_RSEG_UNDO_SLOTS (8 + FLST_BASE_NODE_SIZE + FSEG_HEADER_SIZE)
/* Undo log segment slots */
上面这些信息的存储,是从页面偏移38的位置开始的,在这个位置之前,存储的是文件管理的信息(讲参考索引管理相关章节),从38开始,存储了上面5个信息,它们的意义分别如下:
- TRX_RSEG_MAX_SIZE:回滚段管理页面的总数量,即所有undo段页面之和,一般为ULINT_MAX,即无上限。
- TRX_RSEG_HISTORY_SIZE:这个表来表示当前InnoDB中,在History List中有多少页面,即需要做PURGE的回滚段页面个数。
- TRX_RSEG_HISTORY:这个用来存储History List的链表首地址,事务提交之后,其对应的回滚段如果还不能PURGE,那都会加入到这个链表中。
- TRX_RSEG_FSEG_HEADER:这个用来存储回滚段的Inode位置信息,通过这个地址,就可以找到这个段的详细信息。
- TRX_RSEG_UNDO_SLOTS:这个位置所存储的是一个数组,长度为1024,每一个元素是一个页面号,初始化为FIL_NULL,即空页面。
这5个信息,存储了一个回滚段的信息,最后一个位置的数组,就是用来真正存储回滚段的位置,我们后面会讲到这128*1024个槽是如何使用的。
根据上面的讲述,我们现在已经知道所有回滚段的存储架构了,如下图所示:
现在就可以知道,InnoDB中支持的回滚段总共有128*1024=131072个,TRX_RSEG_UNDO_SLOTS数组的每个元素指向一个页面,这个页面对应一个段,页面号就是段首页的页面号。
在每一个事务开始的时候,都会分配一个rseg,就是从长度为128的数组中,根据最近使用的情况,找到一个临近位置的rseg,在这个事务的生命周期内,被分配的rseg就会被这个事务所使用。
在事务执行过程中,会产生两种回滚日志,一种是INSERT的UNDO记录,一种是UPDATE的UNDO记录,可能有人会问DELETE哪去了?其实是包含在UPDATE的回滚记录中的,因为InnoDB把UNDO分为两类,一类就是新增,也就是INSERT,一类就是修改,就是UPDATE,分类的依据就是事务提交后要不要做PURGE操作,因为INSERT是不需要PURGE的,只要事务提交了,那这个回滚记录就可以丢掉了,而对于更新和删除操作而言,如果事务提交了,还需要为MVCC服务,那就需要将这些日志放到History List中去,等待去做PURGE,以及MVCC的多版本查询等,所以分为两类。
所以,一个事务被分配了一个rseg之后,通常情况下,如果一个事务中既有插入,又有更新(或删除),那这个事务就会对应两个UNDO段,即在一个rseg的1024个槽中,要使用两个槽来存储这个事务的回滚段,一个是插入段,一个是更新段。
在事务要存储回滚记录的时候,事务就要从1024个槽中,根据相应的更新类型(插入或者更新)找到空闲的槽来作为自己的UNDO段。如果已经申请过相同类型的UNDO段,就直接使用,否则就需要新创建一个段,并将段首页号写入到这个rseg的长度为1024的数组的对应位置(空闲位置)中去,这样就将具体的回滚段与整个架构联系起来了。
如果在1024个槽中找不到空闲的位置,那这个事务就会被回滚掉,报出错误为:“Too many active concurrent transactions”,错误号为1637的异常。当然这种情况一般不会见到,如果能把这个用完,估计数据库已经根本动不了了。
上面讲述了整个回滚段存储架构及与事务的相关性,具体到一个事务所使用的某个回滚段的管理,就存储在了回滚段首页中,管理信息包括三部分,分别是Undo page header、Undo segment header及Undo log header。下面分别介绍:
Undo page header:
/** Transaction undo log page header offsets */
#define TRX_UNDO_PAGE_TYPE 0 /*!< TRX_UNDO_INSERT or
TRX_UNDO_UPDATE */
#define TRX_UNDO_PAGE_START 2 /*!< Byte offset where the undo log
records for the LATEST transaction
start on this page (remember that
in an update undo log, the first page
can contain several undo logs) */
#define TRX_UNDO_PAGE_FREE 4 /*!< On each page of the undo log this
field contains the byte offset of the
first free byte on the page */
#define TRX_UNDO_PAGE_NODE 6 /*!< The file list node in the chain
of undo log pages */
- 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段中所有的页面,通过一个双向链表来管理,这个位置存储的就是双向链表的指针。
Undo segment header:
/** Undo log segment header */
#define TRX_UNDO_STATE 0 /*!< TRX_UNDO_ACTIVE, … */
#define TRX_UNDO_LAST_LOG 2 /*!< Offset of the last undo log header
on the segment header page, 0 if
none */
#define TRX_UNDO_FSEG_HEADER 4 /*!< Header for the file segment which
the undo log segment occupies */
#define TRX_UNDO_PAGE_LIST (4 + FSEG_HEADER_SIZE)
/*!< Base node for the list of pages in
the undo log segment; defined only on
the undo log segment’s first page */
- 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则是每个节点的双链指针。
Undo log header:
/** The undo log header. There can be several undo log headers on the first
page of an update undo log segment. */
#define TRX_UNDO_TRX_ID 0 /*!< Transaction id */
#define TRX_UNDO_TRX_NO 8 /*!< Transaction number of the
transaction; defined only if the log
is in a history list */
#define TRX_UNDO_DEL_MARKS 16 /*!< Defined only in an update undo
log: TRUE if the transaction may have
done delete markings of records, and
thus purge is necessary */
#define TRX_UNDO_LOG_START 18 /*!< Offset of the first undo log record
of this log on the header page; purge
may remove undo log record from the
log start, and therefore this is not
necessarily the same as this log
header end offset */
#define TRX_UNDO_XID_EXISTS 20 /*!< TRUE if undo log header includes
X/Open XA transaction identification
XID */
#define TRX_UNDO_DICT_TRANS 21 /*!< TRUE if the transaction is a table
create, index create, or drop
transaction: in recovery
the transaction cannot be rolled back
in the usual way: a ‘rollback’ rather
means dropping the created or dropped
table, if it still exists */
#define TRX_UNDO_TABLE_ID 22 /*!< Id of the table if the preceding
field is TRUE */
#define TRX_UNDO_NEXT_LOG 30 /*!< Offset of the next undo log header
on this page, 0 if none */
#define TRX_UNDO_PREV_LOG 32 /*!< Offset of the previous undo log
header on this page, 0 if none */
#define TRX_UNDO_HISTORY_NODE 34 /*!< If the log is put to the history
list, the file list node is here */
这是一个针对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:用来存储在History List中的双向链表指针。而这个链表的首地址,是在之前我们所介绍的TRX_RSEG_HISTORY位置,可以回到前面去查看相关信息。
到现在为止,关于具体一个UNDO段中每个页面及页面内容是如何管理的已经清楚了,当一个事务需要写入UNDO日志时,就可以直接从对应的UNDO段中找到一个页面及对应的追加日志的偏移位置,然后将对应的UNDO日志写入即可。
(还是昨天那个球,线下讨论了关于会源码的作用,有很多人认为,一提到某人会源码,就觉得一般是用来装一下的,并且实际上没有几家公司强大到放心地让你写源码去,所以觉得这不是一个名副其实的技能。我认为实则不然,会源码,99%的机会不是用来写源码的,而是阅读其实际方法及原理,尽可能的做到知MySQL,或者在出现问题之后,是一个很好的用来解决问题的方法,要知道,我不只一次碰到手册上面所述内容是错误的,并且有很多问题网上也是没有的,那可想而知,如果你会阅读源码了,这样的问题可以立刻迎刃而解)
篇外续
日志这篇,以这样的角度及思维模式来讲述真的是没有见过的,并且到今天已经是连载第七篇,
可以很确定的告诉大家,还有几篇明天继续发。
就在今天,关于这篇内容,我们的韩朱忠老师也说话了:
他也是我们本书的推荐序之一。在这里谢谢他。
文章来自微信公众号:DBAce
本文链接:http://www.yunweipai.com/15841.html