InnoDB 如果发生意外宕机了, 数据会丢么?
对于这个问题, 稍微了解一点 MySQL 知识的人, 都会斩钉截铁的回答: 不会!
为什么?
他们也会毫不犹豫地说: 因为有重做日志(redo log), 数据可以通过 redo log 进行恢复.
回答得很好, 那么 InnoDB 怎样通过 redo log 进行数据恢复的, 具体的流程是怎样的?
估计能说清楚这个问题的人所剩不多了, 更深入一点: 除了 redo log,InnoDB 在恢复过程中, 还需要其他信息么? 比如是否需要 binlog 参与? undo 日志在恢复过程中又会起到什么作用?
到这里, 可能很多人会变得疑惑起来: 数据恢复跟 undo 有半毛钱的关系?
其实, InnoDB 的数据恢复是一个很复杂的过程, 这个恢复过程需要 redo log,binlog,undo log 等参与. 这里把 InnoDB 的恢复过程主要划分为两个阶段:
第一阶段主要依赖于 redo log 的恢复;
而第二阶段, 恰恰需要 binlog 和 undo log 的共同参与.
接下来, 我们来具体了解下整个恢复的过程:
一, 依赖 redo log 进行恢复
第一阶段, 数据库启动后, InnoDB 会通过 redo log 找到最近一次 checkpoint 的位置, 然后根据 checkpoint 相对应的 LSN 开始, 获取需要重做的日志, 接着解析获取的日志并且保存到一个哈希表中, 最后通过遍历哈希表中的 redo log 信息, 读取相关页进行恢复.
InnoDB 的 checkpoint 信息保存在日志文件中, 即 ib_logfile0 的开始 2048 个字节中, checkpoint 有两个, 交替更新, checkpoint 与日志文件的关系如下图:
(checkpoint 位置)
checkpoint 信息分别保存在 ib_logfile0 的 512 字节和 1536 字节处, 每个 checkpoint 默认大小为 512 字节, InnoDB 的 checkpoint 主要由 3 部分信息组成:
checkpoint no: 主要保存的是 checkpoint 号, 因为 InnoDB 有两个 checkpoint, 通过 checkpoint 号来判断哪个 checkpoint 更新.
checkpoint lsn: 主要记录了产生该 checkpoint 是 flush 的 LSN, 确保在该 LSN 前面的数据页都已经落盘, 不再需要通过 redo log 进行恢复.
checkpoint offset: 主要记录了该 checkpoint 产生时, redo log 在 ib_logfile 中的偏移量, 通过该 offset 位置就可以找到需要恢复的 redo log 开始位置.
通过以上 checkpoint 的信息, 我们可以简单得到需要恢复的 redo log 的位置, 然后通过顺序扫描该 redo log 来读取数据, 比如我们通过 checkpoint 定位到开始恢复的 redo log 位置在 ib_logfile1 中的某个位置, 那么整个 redo log 扫描的过程可能是这样的:
(redo log 扫描过程)
Step 1: 从 ib_logfile1 的指定位置开始读取 redo log, 每次读取 4 * page_size 的大小, 这里我们默认页面大小为 16K, 所以每次读取 64K 的 redo log 到缓存中, redo log 每条记录 (block) 的大小为 512 字节.
Step 2: 读取到缓存中的 redo log 通过解析, 验证等一系列过程后, 把 redo log 的内容部分保存到用于恢复的缓存 recv_sys->buf, 保存到恢复缓存中的每条信息主要包含两部分:(space,offset)组成的位置信息和具体 redo log 的内容, 我们称之为 body.
Step 3: 同时保存在恢复缓存中的 redo 信息会根据 (space,offset) 计算一个哈希值后保存到一个哈希表 (recv_sys->addr_hash) 中, 相同哈希值, 不同 (space,offset) 用链表存储, 相同的 (space,offset) 用列表保存, 可能部分事务比较大, redo 信息一个 block 不能保存, 所以, 每个 body 中可以用链表链接多 body 的值.
redo log 被保存到哈希表中之后, InnoDB 就可以开始进行数据恢复, 只需要轮询哈希表中的每个节点获取 redo 信息, 根据 (space,offset) 读取指定页面后进行日志覆盖.
在上面整个过程中, InnoDB 为了保证恢复的速度, 做了几点优化:
优化 1:
在根据 (space,offset) 读取数据页信息到 buffer pool 的时候, InnoDB 不是只读取一张页面, 而是读取相邻的 32 张页面到 buffer pool. 这里有个假设, InnoDB 认为, 如果一张页面被修改了, 那么其周围的一些页面很有可能也被修改了, 所以一次性连续读入 32 张页面可以避免后续再重新读取.
优化 2:
在 MySQL5.7 版本以前, InnoDB 恢复时需要依赖数据字典, 因为 InnoDB 根本不知道某个具体的 space 对应的 ibd 文件是哪个, 这些信息都是数据字典维护的. 而且在恢复前, 需要把所有的表空间全部打开, 如果库中有数以万计的表, 把所有表打开一遍, 整个过程就会很慢. 那么 MySQL5.7 在这上面做了哪些改进呢?
其实很简单, 针对上面的问题, InnoDB 在 redo log 中增加了两种 redo log 的类型来解决.
MLOG_FILE_NAME
用于记录在 checkpoint 之后, 所有被修改过的信息(space,filepath);
MLOG_CHECKPOINT
则用于标志 MLOG_FILE_NAME 的结束.
上面两种 redo log 类型的添加, 完美解决了前面遗留的问题, redo log 中保存了后续需要恢复的 space 和 filepath 对. 所以, 在恢复的时候, 只需要从 checkpoint 的位置一直往后扫描到 MLOG_CHECKPOINT 的位置, 这样就能获取到需要恢复的 space 和 filepath. 在恢复过程中, 只需要打开这些 ibd 文件即可. 当然由于 space 和 filepath 的对应关系通过 redo 存了下来, 恢复的时候也不再依赖数据字典.
这里需要强调的是 MLOG_CHECKPOINT 在每个 checkpoint 点中最多存在一次, 如果出现多次 MLOG_CHECKPOINT 类型的日志, 则说明 redo 已经损坏, InnoDB 会报错.
最多存在一次, 那么会不会有不存在的情况?
答案是肯定的, 在每次 checkpoint 过后, 如果没有发生数据更新, 那么 MLOG_CHECKPOINT 就不会被记录. 所以只要查找下 redo log 最新一个 checkpoint 后的 MLOG_CHECKPOINT 是否存在, 就能判定上次 MySQL 是否正常关机.
5.7 版本的 MySQL 在 InnoDB 进行恢复的时候, 也正是这样做的, MySQL5.7 在进行恢复的时候, 一般情况下需要进行最多 3 次的 redo log 扫描:
1, 首先对 redo log 的扫描, 主要是为了查找 MLOG_CHECKPOINT, 这里并不进行 redo log 的解析. 如果你没有找到 MLOG_CHECKPOINT, 则说明 InnoDB 不需要进行 recovery, 后面的两次扫描可以省略; 如果找到了 MLOG_CHECKPOINT, 则获取 MLOG_FILE_NAME 到指定列表, 后续只需打开该链表中的表空间即可.
2, 下一步的扫描是在第一次找到 MLOG_CHECKPOINT 基础之上进行的, 该次扫描会把 redo log 解析到哈希表中, 如果扫描完整个文件, 哈希表还没有被填满, 则不需要第三次扫描, 直接进行 recovery 就结束.
3, 最后是在第二次基础上进行的, 第二次扫描把哈希表填满后, 还有 redo log 剩余, 则需要循环进行扫描, 哈希表满后立即进行 recovery, 直到所有的 redo log 被 apply 完为止.
redo log 全部被解析并且 apply 完成, 整个 InnoDB recovery 的第一阶段也就结束了, 在该阶段中, 所有已经被记录到 redo log 但是没有完成数据刷盘的记录都被重新落盘.
然而, InnoDB 单靠 redo log 的恢复是不够的, 这样还是有可能会丢失数据(或者说造成主从数据不一致).
因为在事务提交过程中, 写 binlog 和写 redo log 提交是两个过程, 写 binlog 在前而 redo 提交在后, 如果 MySQL 写完 binlog 后, 在 redo 提交之前发生了宕机, 这样就会出现问题: binlog 中已经包含了该条记录, 而 redo 没有持久化. binlog 已经落盘就意味着 slave 上可以 apply 该条数据, redo 没有持久化则代表了 master 上该条数据并没有落盘, 也不能通过 redo 进行恢复.
这样就造成了主从数据的不一致, 换句话说主上丢失了部分数据, 那么 MySQL 又是如何保证在这样的情况下, 数据还是一致的? 这就需要进行第二阶段恢复.
二, binlog 和 undo log 共同参与
前面提到, 在第二阶段恢复中, 需要用到 binlog 和 undo log, 下面我们就来看下具体的恢复逻辑是怎样的?
其实该阶段的恢复中, 也被划分成两部分: 第一部分, 根据 binlog 获取所有可能没有提交事务的 xid 列表; 第二部分, 根据 undo 中的信息构造所有未提交事务链表, 最后通过上面两部分协调判断事务是否可以提交.
(根据 binlog 获取 xid 列表)
如上图所示, MySQL 在第二阶段恢复的时候, 先会去读取最后一个 binlog 文件的所有 event 信息, 然后把 xid 保存到一个列表中, 然后进行第二部分的恢复, 如下:
(基于 undo 构造事务链表)
我们知道, InnoDB 当前版本有 128 个回滚段, 每个回滚段中保存了 undo log 的位置指针, 通过扫描 undo 日志, 我们可以构造出还未被提交的事务链表 (存在于 insert_undo_list 和 update_undo_lsit 中的事务都是未被提交的), 所以通过起始页(0,5) 下的 solt 信息可以定位到回滚段, 然后根据回滚段下的 undo 的 slot 定位到 undo 页, 把所有的 undo 信息构建一个 undo_list, 然后通过 undo_list 再创建未提交事务链表 trx_sys->trx_list.
基于上面两步, 我们已经构建了 xid 列表和未提交事务列表, 那么在这些未提交事务列表中的事务, 哪些需要被提交? 哪些又该回滚?
判断条件很简单: 凡是 xid 在通过 binlog 构建的 xid 列表中存在的事务, 都需要被提交. 换句话说, 所有已经记录 binlog 的事务, 需要被提交, 而剩下那些没有记录 binlog 的事务, 则需要被回滚.
三, 回顾优化
通过上述两个阶段的数据恢复, InnoDB 才最终完成整个 recovery 过程, 回过头来我们再想想, 在上述两个阶段中, 是否还有优化空间? 比如第一阶段, 在构造完哈希表后, 事务的恢复是否可以并发进行? 理论上每个 hash node 是根据 (space,offset) 生成的, 不同的 hash node 之间不存在冲突, 可以并行进行恢复.
或者在根据哈希表进行数据页读取时, 每次读取连续 32 张页面, 这里读取的 32 张页面, 可能有部分是不需要的, 也同时被读入到 Buffer Pool 中了, 是否可以在构建一颗红黑树, 根据 (space,offset) 组合键进行插入, 这样如果需要恢复的时候, 可以根据红黑树的排序原理, 把所有页面的读取顺序化, 并不需要读取额外的页面.
来源: http://www.jianshu.com/p/80c28fe4e2fe