learn.lianglianglee.com/文章/MySQL · 引擎特性 · InnoDB崩溃恢复.md.html
2022-05-11 19:04:14 +08:00

975 lines
50 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>MySQL · 引擎特性 · InnoDB崩溃恢复.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/文章/AQS 万字图文全面解析.md.html">AQS 万字图文全面解析.md.html</a>
</li>
<li>
<a href="/文章/Docker 镜像构建原理及源码分析.md.html">Docker 镜像构建原理及源码分析.md.html</a>
</li>
<li>
<a href="/文章/ElasticSearch 小白从入门到精通.md.html">ElasticSearch 小白从入门到精通.md.html</a>
</li>
<li>
<a href="/文章/JVM CPU Profiler技术原理及源码深度解析.md.html">JVM CPU Profiler技术原理及源码深度解析.md.html</a>
</li>
<li>
<a href="/文章/JVM 垃圾收集器.md.html">JVM 垃圾收集器.md.html</a>
</li>
<li>
<a href="/文章/JVM 面试的 30 个知识点.md.html">JVM 面试的 30 个知识点.md.html</a>
</li>
<li>
<a href="/文章/Java IO 体系、线程模型大总结.md.html">Java IO 体系、线程模型大总结.md.html</a>
</li>
<li>
<a href="/文章/Java NIO浅析.md.html">Java NIO浅析.md.html</a>
</li>
<li>
<a href="/文章/Java 面试题集锦(网络篇).md.html">Java 面试题集锦(网络篇).md.html</a>
</li>
<li>
<a href="/文章/Java-直接内存 DirectMemory 详解.md.html">Java-直接内存 DirectMemory 详解.md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md.html">Java中9种常见的CMS GC问题分析与解决.md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决.md.html">Java中9种常见的CMS GC问题分析与解决.md.html</a>
</li>
<li>
<a href="/文章/Java中的SPI.md.html">Java中的SPI.md.html</a>
</li>
<li>
<a href="/文章/Java中的ThreadLocal.md.html">Java中的ThreadLocal.md.html</a>
</li>
<li>
<a href="/文章/Java线程池实现原理及其在美团业务中的实践.md.html">Java线程池实现原理及其在美团业务中的实践.md.html</a>
</li>
<li>
<a href="/文章/Java魔法类Unsafe应用解析.md.html">Java魔法类Unsafe应用解析.md.html</a>
</li>
<li>
<a href="/文章/Kafka 源码阅读笔记.md.html">Kafka 源码阅读笔记.md.html</a>
</li>
<li>
<a href="/文章/Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html">Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB Buffer Pool.md.html">MySQL · 引擎特性 · InnoDB Buffer Pool.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB IO子系统.md.html">MySQL · 引擎特性 · InnoDB IO子系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 事务系统.md.html">MySQL · 引擎特性 · InnoDB 事务系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 同步机制.md.html">MySQL · 引擎特性 · InnoDB 同步机制.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 数据页解析.md.html">MySQL · 引擎特性 · InnoDB 数据页解析.md.html</a>
</li>
<li>
<a class="current-tab" href="/文章/MySQL · 引擎特性 · InnoDB崩溃恢复.md.html">MySQL · 引擎特性 · InnoDB崩溃恢复.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · 临时表那些事儿.md.html">MySQL · 引擎特性 · 临时表那些事儿.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 半同步复制.md.html">MySQL 主从复制 半同步复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 基于GTID复制.md.html">MySQL 主从复制 基于GTID复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制.md.html">MySQL 主从复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 事务日志(redo log和undo log).md.html">MySQL 事务日志(redo log和undo log).md.html</a>
</li>
<li>
<a href="/文章/MySQL 亿级别数据迁移实战代码分享.md.html">MySQL 亿级别数据迁移实战代码分享.md.html</a>
</li>
<li>
<a href="/文章/MySQL 从一条数据说起-InnoDB行存储数据结构.md.html">MySQL 从一条数据说起-InnoDB行存储数据结构.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:事务和锁的面纱.md.html">MySQL 地基基础:事务和锁的面纱.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据字典.md.html">MySQL 地基基础:数据字典.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据库字符集.md.html">MySQL 地基基础:数据库字符集.md.html</a>
</li>
<li>
<a href="/文章/MySQL 性能优化:碎片整理.md.html">MySQL 性能优化:碎片整理.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html">MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:如何在日志中轻松定位大事务.md.html">MySQL 故障诊断:如何在日志中轻松定位大事务.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:教你快速定位加锁的 SQL.md.html">MySQL 故障诊断:教你快速定位加锁的 SQL.md.html</a>
</li>
<li>
<a href="/文章/MySQL 日志详解.md.html">MySQL 日志详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL 的半同步是什么?.md.html">MySQL 的半同步是什么?.md.html</a>
</li>
<li>
<a href="/文章/MySQL中的事务和MVCC.md.html">MySQL中的事务和MVCC.md.html</a>
</li>
<li>
<a href="/文章/MySQL事务_事务隔离级别详解.md.html">MySQL事务_事务隔离级别详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL优化优化 select count().md.html">MySQL优化优化 select count().md.html</a>
</li>
<li>
<a href="/文章/MySQL共享锁、排他锁、悲观锁、乐观锁.md.html">MySQL共享锁、排他锁、悲观锁、乐观锁.md.html</a>
</li>
<li>
<a href="/文章/MySQL的MVCC多版本并发控制.md.html">MySQL的MVCC多版本并发控制.md.html</a>
</li>
<li>
<a href="/文章/QingStor 对象存储架构设计及最佳实践.md.html">QingStor 对象存储架构设计及最佳实践.md.html</a>
</li>
<li>
<a href="/文章/RocketMQ 面试题集锦.md.html">RocketMQ 面试题集锦.md.html</a>
</li>
<li>
<a href="/文章/SnowFlake 雪花算法生成分布式 ID.md.html">SnowFlake 雪花算法生成分布式 ID.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html">Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 教程:如何开发一个 starter.md.html">Spring Boot 教程:如何开发一个 starter.md.html</a>
</li>
<li>
<a href="/文章/Spring MVC 原理.md.html">Spring MVC 原理.md.html</a>
</li>
<li>
<a href="/文章/Spring MyBatis和Spring整合的奥秘.md.html">Spring MyBatis和Spring整合的奥秘.md.html</a>
</li>
<li>
<a href="/文章/Spring 帮助你更好的理解Spring循环依赖.md.html">Spring 帮助你更好的理解Spring循环依赖.md.html</a>
</li>
<li>
<a href="/文章/Spring 循环依赖及解决方式.md.html">Spring 循环依赖及解决方式.md.html</a>
</li>
<li>
<a href="/文章/Spring中眼花缭乱的BeanDefinition.md.html">Spring中眼花缭乱的BeanDefinition.md.html</a>
</li>
<li>
<a href="/文章/Vert.x 基础入门.md.html">Vert.x 基础入门.md.html</a>
</li>
<li>
<a href="/文章/eBay 的 Elasticsearch 性能调优实践.md.html">eBay 的 Elasticsearch 性能调优实践.md.html</a>
</li>
<li>
<a href="/文章/不可不说的Java“锁”事.md.html">不可不说的Java“锁”事.md.html</a>
</li>
<li>
<a href="/文章/互联网并发限流实战.md.html">互联网并发限流实战.md.html</a>
</li>
<li>
<a href="/文章/从ReentrantLock的实现看AQS的原理及应用.md.html">从ReentrantLock的实现看AQS的原理及应用.md.html</a>
</li>
<li>
<a href="/文章/从SpringCloud开始聊微服务架构.md.html">从SpringCloud开始聊微服务架构.md.html</a>
</li>
<li>
<a href="/文章/全面了解 JDK 线程池实现原理.md.html">全面了解 JDK 线程池实现原理.md.html</a>
</li>
<li>
<a href="/文章/分布式一致性理论与算法.md.html">分布式一致性理论与算法.md.html</a>
</li>
<li>
<a href="/文章/分布式一致性算法 Raft.md.html">分布式一致性算法 Raft.md.html</a>
</li>
<li>
<a href="/文章/分布式唯一 ID 解析.md.html">分布式唯一 ID 解析.md.html</a>
</li>
<li>
<a href="/文章/分布式链路追踪:集群管理设计.md.html">分布式链路追踪:集群管理设计.md.html</a>
</li>
<li>
<a href="/文章/动态代理种类及原理,你知道多少?.md.html">动态代理种类及原理,你知道多少?.md.html</a>
</li>
<li>
<a href="/文章/响应式架构与 RxJava 在有赞零售的实践.md.html">响应式架构与 RxJava 在有赞零售的实践.md.html</a>
</li>
<li>
<a href="/文章/大数据算法——布隆过滤器.md.html">大数据算法——布隆过滤器.md.html</a>
</li>
<li>
<a href="/文章/如何优雅地记录操作日志?.md.html">如何优雅地记录操作日志?.md.html</a>
</li>
<li>
<a href="/文章/如何设计一个亿级消息量的 IM 系统.md.html">如何设计一个亿级消息量的 IM 系统.md.html</a>
</li>
<li>
<a href="/文章/异步网络模型.md.html">异步网络模型.md.html</a>
</li>
<li>
<a href="/文章/当我们在讨论CQRS时我们在讨论些神马.md.html">当我们在讨论CQRS时我们在讨论些神马.md.html</a>
</li>
<li>
<a href="/文章/彻底理解 MySQL 的索引机制.md.html">彻底理解 MySQL 的索引机制.md.html</a>
</li>
<li>
<a href="/文章/最全的 116 道 Redis 面试题解答.md.html">最全的 116 道 Redis 面试题解答.md.html</a>
</li>
<li>
<a href="/文章/有赞权限系统(SAM).md.html">有赞权限系统(SAM).md.html</a>
</li>
<li>
<a href="/文章/有赞零售中台建设方法的探索与实践.md.html">有赞零售中台建设方法的探索与实践.md.html</a>
</li>
<li>
<a href="/文章/服务注册与发现原理剖析Eureka、Zookeeper、Nacos.md.html">服务注册与发现原理剖析Eureka、Zookeeper、Nacos.md.html</a>
</li>
<li>
<a href="/文章/深入浅出Cache.md.html">深入浅出Cache.md.html</a>
</li>
<li>
<a href="/文章/深入理解 MySQL 底层实现.md.html">深入理解 MySQL 底层实现.md.html</a>
</li>
<li>
<a href="/文章/漫画讲解 git rebase VS git merge.md.html">漫画讲解 git rebase VS git merge.md.html</a>
</li>
<li>
<a href="/文章/生成浏览器唯一稳定 ID 的探索.md.html">生成浏览器唯一稳定 ID 的探索.md.html</a>
</li>
<li>
<a href="/文章/缓存 如何保证缓存与数据库的双写一致性?.md.html">缓存 如何保证缓存与数据库的双写一致性?.md.html</a>
</li>
<li>
<a href="/文章/网易严选怎么做全链路监控的?.md.html">网易严选怎么做全链路监控的?.md.html</a>
</li>
<li>
<a href="/文章/美团万亿级 KV 存储架构与实践.md.html">美团万亿级 KV 存储架构与实践.md.html</a>
</li>
<li>
<a href="/文章/美团点评Kubernetes集群管理实践.md.html">美团点评Kubernetes集群管理实践.md.html</a>
</li>
<li>
<a href="/文章/美团百亿规模API网关服务Shepherd的设计与实现.md.html">美团百亿规模API网关服务Shepherd的设计与实现.md.html</a>
</li>
<li>
<a href="/文章/解读《阿里巴巴 Java 开发手册》背后的思考.md.html">解读《阿里巴巴 Java 开发手册》背后的思考.md.html</a>
</li>
<li>
<a href="/文章/认识 MySQL 和 Redis 的数据一致性问题.md.html">认识 MySQL 和 Redis 的数据一致性问题.md.html</a>
</li>
<li>
<a href="/文章/进阶Dockerfile 高阶使用指南及镜像优化.md.html">进阶Dockerfile 高阶使用指南及镜像优化.md.html</a>
</li>
<li>
<a href="/文章/铁总在用的高性能分布式缓存计算框架 Geode.md.html">铁总在用的高性能分布式缓存计算框架 Geode.md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析.md.html</a>
</li>
<li>
<a href="/文章/面试最常被问的 Java 后端题.md.html">面试最常被问的 Java 后端题.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计在互联网业务开发中的实践.md.html">领域驱动设计在互联网业务开发中的实践.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计的菱形对称架构.md.html">领域驱动设计的菱形对称架构.md.html</a>
</li>
<li>
<a href="/文章/高效构建 Docker 镜像的最佳实践.md.html">高效构建 Docker 镜像的最佳实践.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>MySQL · 引擎特性 · InnoDB崩溃恢复</h1>
<h2>前言</h2>
<p>数据库系统与文件系统最大的区别在于数据库能保证操作的原子性一个操作要么不做要么都做即使在数据库宕机的情况下也不会出现操作一半的情况这个就需要数据库的日志和一套完善的崩溃恢复机制来保证。本文仔细剖析了InnoDB的崩溃恢复流程代码基于5.6分支。</p>
<h2>基础知识</h2>
<p>**lsn: ** 可以理解为数据库从创建以来产生的redo日志量这个值越大说明数据库的更新越多也可以理解为更新的时刻。此外每个数据页上也有一个lsn表示最后被修改时的lsn值越大表示越晚被修改。比如数据页A的lsn为100数据页B的lsn为200checkpoint lsn为150系统lsn为300表示当前系统已经更新到300小于150的数据页已经被刷到磁盘上因此数据页A的最新数据一定在磁盘上而数据页B则不一定有可能还在内存中。
**redo日志: ** 现代数据库都需要写redo日志例如修改一条数据首先写redo日志然后再写数据。在写完redo日志后就直接给客户端返回成功。这样虽然看过去多写了一次盘但是由于把对磁盘的随机写入(写数据)转换成了顺序的写入(写redo日志)性能有很大幅度的提高。当数据库挂了之后通过扫描redo日志就能找出那些没有刷盘的数据页(在崩溃之前可能数据页仅仅在内存中修改了,但是还没来得及写盘),保证数据不丢。
**undo日志: ** 数据库还提供类似撤销的功能当你发现修改错一些数据时可以使用rollback指令回滚之前的操作。这个功能需要undo日志来支持。此外现代的关系型数据库为了提高并发(同一条记录,不同线程的读取不冲突,读写和写读不冲突,只有同时写才冲突)都实现了类似MVCC的机制在InnoDB中这个也依赖undo日志。为了实现统一的管理与redo日志不同undo日志在Buffer Pool中有对应的数据页与普通的数据页一起管理依据LRU规则也会被淘汰出内存后续再从磁盘读取。与普通的数据页一样对undo页的修改也需要先写redo日志。
**检查点: ** 英文名为checkpoint。数据库为了提高性能数据页在内存修改后并不是每次都会刷到磁盘上。checkpoint之前的数据页保证一定落盘了这样之前的日志就没有用了(由于InnoDB redolog日志循环使用这时这部分日志就可以被覆盖)checkpoint之后的数据页有可能落盘也有可能没有落盘所以checkpoint之后的日志在崩溃恢复的时候还是需要被使用的。InnoDB会依据脏页的刷新情况定期推进checkpoint从而减少数据库崩溃恢复的时间。检查点的信息在第一个日志文件的头部。
**崩溃恢复: ** 用户修改了数据并且收到了成功的消息然而对数据库来说可能这个时候修改后的数据还没有落盘如果这时候数据库挂了重启后数据库需要从日志中把这些修改后的数据给捞出来重新写入磁盘保证用户的数据不丢。这个从日志中捞数据的过程就是崩溃恢复的主要任务也可以成为数据库前滚。当然在崩溃恢复中还需要回滚没有提交的事务提交没有提交成功的事务。由于回滚操作需要undo日志的支持undo日志的完整性和可靠性需要redo日志来保证所以崩溃恢复先做redo前滚然后做undo回滚。</p>
<p>我们从源码角度仔细剖析一下数据库崩溃恢复过程。整个过程都在引擎初始化阶段完成(<code>innobase_init</code>),其中最主要的函数是<code>innobase_start_or_create_for_mysql</code>innodb通过这个函数完成创建和初始化包括崩溃恢复。首先来介绍一下数据库的前滚。</p>
<h2>redo日志前滚数据库</h2>
<p>前滚数据库主要分为两阶段首先是日志扫描阶段扫描阶段按照数据页的space_id和page_no分发redo日志到hash_table中保证同一个数据页的日志被分发到同一个哈希桶中且按照lsn大小从小到大排序。扫描完后再遍历整个哈希表依次应用每个数据页的日志应用完后在数据页的状态上至少恢复到了崩溃之前的状态。我们来详细分析一下代码。
首先打开所有的ibdata文件(<code>open_or_create_data_files</code>)(ibdata可以有多个)每个ibdata文件有个flush_lsn在头部计算出这些文件中的max_flush_lsn和min_flush_lsn因为ibdata也有可能有数据没写完整需要恢复后续(<code>recv_recovery_from_checkpoint_start_func</code>)通过比较checkpont_lsn和这两个值来确定是否需要对ibdata前滚。
接着,打开系统表空间和日志表空间的所有文件(<code>fil_open_log_and_system_tablespace_files</code>)防止出现文件句柄不足清空buffer pool(<code>buf_pool_invalidate</code>)。接下来就进入最最核心的函数:recv_recovery_from_checkpoint_start_func注意即使数据库是正常关闭的也会进入。
虽然<code>recv_recovery_from_checkpoint_start_func</code>看过去很冗长但是很多代码都是为了LOG_ARCHIVE特性而编写的真正数据崩溃恢复的代码其实不多。
首先,初始化一些变量,查看<code>srv_force_recovery</code>这个变量,如果用户设置跳过前滚阶段,函数直接返回。
接着,初始化<code>recv_sys</code>结构分配hash_table的大小同时初始化flush list rbtree。<code>recv_sys</code>结构主要在崩溃恢复前滚阶段使用。hash_table就是之前说的用来存不同数据页日志的哈希表哈希表的大小被初始化为buffer_size_in_bytes/512, 这个是哈希表最大的长度超过就存不下了幸运的是需要恢复的数据页的个数不会超过这个值因为buffer poll最多(数据库崩溃之前脏页的上线)只能存放buffer_size_in_bytes/16KB个数据页即使考虑压缩页最多也只有buffer_size_in_bytes/1KB个此外关于这个哈希表内存分配的大小可以参考bug#53122。flush list rbtree这个主要是为了加入插入脏页列表InnoDB的flush list必须按照数据页的最老修改lsn(oldest_modifcation)从小到大排序在数据库正常运行时可以通过log_sys-&gt;mutex和log_sys-&gt;log_flush_order_mutex保证顺序在崩溃恢复则没有这种保证应用数据的时候是从第一个元素开始遍历哈希表不能保证数据页按照最老修改lsn(oldest_modifcation)从小到大排序这样就需要线性遍历flush_list来寻找插入位置效率太低因此引入红黑树加快查找插入的位置。
接着从ib_logfile0的头中读取checkpoint信息主要包括checkpoint_lsn和checkpoint_no。由于InnoDB日志是循环使用的且最少要有2个所以ib_logfile0一定存在把checkpoint信息存在里面很安全不用担心被删除。checkpoint信息其实会写在文件头的两个地方两个checkpoint域轮流写。为什么要两个地方轮流写呢假设只有一个checkpoint域一直更新这个域而checkpoint域有512字节(<code>OS_FILE_LOG_BLOCK_SIZE</code>)如果刚好在写这个512字节的时候数据库挂了服务器也挂了(先不考虑硬件的原子写特性,早期的硬件没有这个特性)这个512字节可能只写了一半导致整个checkpoint域不可用。这样数据库将无法做崩溃恢复从而无法启动。如果有两个checkpoint域那么即使一个写坏了还可以用另外一个尝试恢复虽然有可能这个时候日志已经被覆盖但是至少提高了恢复成功的概率。两个checkpoint域轮流写也能减少磁盘扇区故障带来的影响。checkpoint_lsn之前的数据页都已经落盘不需要前滚之后的数据页可能还没落盘需要重新恢复出来即使已经落盘也没关系因为redo日志时幂等的应用一次和应用两次都一样(底层实现: 如果数据页上的lsn大于等于当前redo日志的lsn就不应用否则应用。checkpoint_no可以理解为checkpoint域写盘的次数每次刷盘递增1同时这个值取模2可以用来实现checkpoint_no域的轮流写。正常逻辑下选取checkpoint_no值大的作为最终的checkpoint信息用来做后续崩溃恢复扫描的起始点。
接着使用checkpoint域的信息初始化recv_sys结构体的一些信息后就进入日志解析的核心函数<code>recv_group_scan_log_recs</code>这个函数后续我们再分析主要作用就是解析redo日志如果内存不够了就直接调用应用(<code>recv_apply_hashed_log_recs</code>)日志,然后再接着解析。如果需要应用的日志很少,就仅仅解析分发日志,到<code>recv_recovery_from_checkpoint_finish</code>函数中在应用日志。
接着依据当前刷盘的数据页状态做一次checkpoint因为在<code>recv_group_scan_log_recs</code>里可能已经应用部分日志了。至此<code>recv_recovery_from_checkpoint_start_func</code>函数结束。
<code>recv_recovery_from_checkpoint_finish</code>函数中如果srv_force_recovery设置正确就开始调用函数<code>recv_apply_hashed_log_recs</code>应用日志,然后等待刷脏的线程退出(线程是崩溃恢复时临时启动的)最后释放recv_sys的相关资源以及hash_table占用的内存。
至此数据库前滚结束。接下来我们详细分析一下redo日志解析函数以及redo日志应用函数的实现细节。</p>
<h2>redo日志解析函数</h2>
<p>解析函数的最上层是<code>recv_group_scan_log_recs</code>,这个函数调用底层函数(<code>log_group_read_log_seg</code>)按照RECV_SCAN_SIZE(64KB)大小分批读取。读取出来后首先通过block_no和lsn之间的关系以及日志checksum判断是否读到了日志最后(所以可以看出,并没一个标记在日志头标记日志的有效位置,完全是按照上述两个条件判断是否到达了日志尾部),如果读到最后则返回(之前说了即使数据库是正常关闭的也要走崩溃恢复逻辑那么在这里就返回了因为正常关闭的checkpoint值一定是指向日志最后)否则则把日志去头掐尾放到一个recv_sys-&gt;buf中日志头里面存了一些控制信息和checksum值只是用来校验和定位在真正的应用中没有用。在放到recv_sys-&gt;buf之前需要检验一下recv_sys-&gt;buf有没有满(<code>RECV_PARSING_BUF_SIZE</code>2M),满了就报错(如果上一批解析有不完整的日志日志解析函数不会分发而是把这些不完整的日志留在recv_sys-&gt;buf中直到解析到完整的日志)。接下的事情就是从recv_sys-&gt;buf中解析日志(<code>recv_parse_log_recs</code>)。日志分两种single_rec和multi_rec前者表示只对一个数据页进行一种操作后者表示对一个或者多个数据页进行多种操作。日志中还包括对应数据页的space_idpage_no操作的type以及操作的内容(<code>recv_parse_log_rec</code>)。解析出相应的日志后按照space_id和page_no进行哈希(如果对应的表空间在内存中不存在,则表示表已经被删除了)放到hash_table里面(日志真正存放的位置依然在buffer pool)即可,等待后续应用。这里有几个点值得注意:</p>
<ul>
<li>如果是multi_rec类型则只有遇到MLOG_MULTI_REC_END这个标记日志才算完整才会被分发到hash_table中。查看代码我们可以发现multi_rec类型的日志被解析了两次一次用来校验完整性(寻找MLOG_MULTI_REC_END),第二次才用来分发日志,感觉这是一个可以优化的点。</li>
<li>目前日志的操作type有50多种每种操作后面的内容都不一样所以长度也不一样目前日志的解析逻辑需要依次解析出所有的内容然后确定长度从而定位下一条日志的开始位置。这种方法效率略低其实可以在每种操作的头上加上一个字段存储后面内容的长度这样就不需要解析太多的内容从而提高解析速度进一步提高崩溃恢复速度从结果看可以提高一倍的速度(从38秒到14秒详情可以参见bug#82937)。</li>
<li>如果发现checkpoint之后还有日志说明数据库之前没有正常关闭需要做崩溃恢复因此需要做一些额外的操作(<code>recv_init_crash_recovery</code>)比如在错误日志中打印我们常见的“Database was not shutdown normally!”和“Starting crash recovery.”还要从double write buffer中检查是否发生了数据页半写如果有需要恢复(<code>buf_dblwr_process</code>),还需要启动一个线程用来刷新应用日志产生的脏页(因为这个时候buf_flush_page_cleaner_thread还没有启动)。最后还需要打开所有的表空间。。注意是所有的表。。。我们在阿里云RDS MySQL的运维中常常发现数据库hang在了崩溃恢复阶段在错误日志中有类似“Reading tablespace information from the .ibd files...”字样这就表示数据库正在打开所有的表然后一看表的数量发现有几十甚至上百万张表。。。数据库之所以要打开所有的表是因为在分发日志的时候需要确定space_id对应哪个ibd文件通过打开所有的表读取space_id信息来确定另外一个原因是方便double write buffer检查半写数据页。针对这个表数量过多导致恢复过慢的问题MySQL 5.7做了优化WL#7142 主要思想就是在每次checkpoint后在第一次修改某个表时先写一个新日志mlog_file_name(包括space_id和filename的映射)来表示对这个表进行了操作后续对这个表的操作就不用写这个新日志了当需要崩溃恢复时候多一次扫描通过搜集mlog_file_name来确定哪些表被修改过这样就不需要打开所有的表来确定space_id了。</li>
<li>最后一个值得注意的地方是内存。之前说过如果有太多的日志已经被分发占用了太多的内存日志解析函数会在适当的时候应用日志而不是等到最后才一起应用。那么问题来了使用了多大的内存就会出发应用日志逻辑。答案是buffer_pool_size_in_bytes - 512 * buffer_pool_instance_num * 16KB。由于buffer_pool_instance_num一般不会太大所以可以任务buffer pool的大部分内存都被用来存放日志。剩下的那些主要留给应用日志时读取的数据页因为目前来说日志应用是单线程的读取一个日志把所有日志应用完然后就可以刷回磁盘了不需要太多的内存。</li>
</ul>
<h2>redo日志应用函数</h2>
<p>应用日志的上层函数为<code>recv_apply_hashed_log_recs</code>(应用日志也可能在io_helper函数中进行)主要作用就是遍历hash_table从磁盘读取对每个数据页依次应用哈希桶中的日志。应用完所有的日志后如果需要则把buffer_pool的页面都刷盘毕竟空间有限。有以下几点值得注意</p>
<ul>
<li>同一个数据页的日志必须按照lsn从小到大应用否则数据会被覆盖。只应用redo日志lsn大于page_lsn的日志只有这些日志需要重做其余的忽略。应用完日志后把脏页加入脏页列表由于脏页列表是按照最老修改lsn(oldest_modification)来排序的,这里通过引入一颗红黑树来加速查找插入的位置,时间复杂度从之前的线性查找降为对数级别。</li>
<li>当需要某个数据页的时候如果发现其没有在Buffer Pool中则会查看这个数据页周围32个数据页是否也需要做恢复如果需要则可以一起读取出来相当于做了一次io合并减少io操作(<code>recv_read_in_area</code>)。由于这个是异步读取所以最终应用日志的活儿是由io_helper线程来做的(<code>buf_page_io_complete</code>)此外为了防止短时间发起太多的io在代码中加了流量控制的逻辑(<code>buf_read_recv_pages</code>)。如果发现某个数据页在内存中,则直接调用<code>recv_recover_page</code>应用日志。由此我们可以看出InnoDB应用日志其实并不是单线程的来应用日志的除了崩溃恢复的主线程外io_helper线程也会参与恢复。并发线程数取决于io_helper中读取线程的个数。</li>
</ul>
<p>执行完了redo前滚数据库数据库的所有数据页已经处于一致的状态undo回滚数据库就可以安全的执行了。数据库崩溃的时候可能有一些没有提交的事务或者已经提交的事务这个时候就需要决定是否提交。主要分为三步首先是扫描undo日志重新建立起undo日志链表接着是依据上一步建立起的链表重建崩溃前的事务即恢复当时事务的状态。最后就是依据事务的不同状态进行回滚或者提交。</p>
<h2>undo日志回滚数据库</h2>
<p><code>recv_recovery_from_checkpoint_start_func</code>之后,<code>recv_recovery_from_checkpoint_finish</code>之前,调用了<code>trx_sys_init_at_db_start</code>,这个函数做了上述三步中的前两步。
第一步在函数<code>trx_rseg_array_init</code>中处理遍历整个undo日志空间(最多TRX_SYS_N_RSEGS(128)个segment)如果发现某个undo segment非空就进行初始化(<code>trx_rseg_create_instance</code>)。整个每个undo segment如果发现undo slot非空(最多TRX_RSEG_N_SLOTS(1024)个slot),也就行初始化(<code>trx_undo_lists_init</code>)。在初始化undo slot后就把不同类型的undo日志放到不同链表中(<code>trx_undo_mem_create_at_db_start</code>)。undo日志主要分为两种TRX_UNDO_INSERT和TRX_UNDO_UPDATE。前者主要是提供给insert操作用的后者是给update和delete操作使用。之前说过undo日志有两种作用事务回滚时候用和MVCC快照读取时候用。由于insert的数据不需要提供给其他线程用所以只要事务提交就可以删除TRX_UNDO_INSERT类型的undo日志。TRX_UNDO_UPDATE在事务提交后还不能删除需要保证没有快照使用它的时候才能通过后台的purge线程清理。
第二步在函数<code>trx_lists_init_at_db_start</code>中进行由于第一步中已经在内存中建立起了undo_insert_list和undo_update_list(链表每个undo segment独立),所以这一步只需要遍历所有链表,重建起事务的状态(<code>trx_resurrect_insert</code><code>trx_resurrect_update</code>)。简单的说如果undo日志的状态是TRX_UNDO_ACTIVE则事务的状态为TRX_ACTIVE如果undo日志的状态是TRX_UNDO_PREPARED则事务的状态为TRX_PREPARED。这里还要考虑变量srv_force_recovery的设置如果这个变量值为非0所有的事务都会回滚(即事务被设置为TRX_ACTIVE)即使事务的状态应该为TRX_STATE_PREPARED。重建起事务后按照事务id加入到trx_sys-&gt;trx_list链表中。最后在函数<code>trx_sys_init_at_db_start</code>中,会统计所有需要回滚的事务(事务状态为TRX_ACTIVE)一共需要回滚多少行数据输出到错误日志中类似5 transaction(s) which must be rolled back or cleaned up。InnoDB: in total 342232 row operations to undo的字样。
第三步的操作在两个地方被调用。一个是在<code>recv_recovery_from_checkpoint_finish</code>的最后,另外一个是在<code>recv_recovery_rollback_active</code>中。前者主要是回滚对数据字典的操作也就是回滚DDL语句的操作后者是回滚DML语句。前者是在数据库可提供服务之前必须完成后者则可以在数据库提供服务(也即是崩溃恢复结束)之后继续进行(通过新开一个后台线程<code>trx_rollback_or_clean_all_recovered</code>来处理)。因为InnoDB认为数据字典是最重要的必须要回滚到一致的状态才行而用户表的数据可以稍微慢一点对外提供服务后慢慢恢复即可。因此我们常常在会发现数据库已经启动起来了然后错误日志中还在不断的打印回滚事务的信息。事务回滚的核心函数是<code>trx_rollback_or_clean_recovered</code>逻辑很简单只需要遍历trx_sys-&gt;trx_list按照事务不同的状态回滚或者提交即可(<code>trx_rollback_resurrected</code>)。这里要注意的是如果事务是TRX_STATE_PREPARED状态那么在InnoDB层不做处理需要在Server层依据binlog的情况来决定是否回滚事务如果binlog已经写了事务就提交因为binlog写了就可能被传到备库如果主库回滚会导致主备数据不一致如果binlog没有写就回滚事务。</p>
<h2>崩溃恢复相关参数解析</h2>
<p>**innodb_fast_shutdown: **
innodb_fast_shutdown = 0。这个表示在MySQL关闭的时候执行slow shutdown不但包括日志的刷盘数据页的刷盘还包括数据的清理(purge)ibuf的合并buffer pool dump以及lazy table drop操作(如果表上有未完成的操作即使执行了drop table且返回成功了表也不一定立刻被删除)。
innodb_fast_shutdown = 1。这个是默认值表示在MySQL关闭的时候仅仅把日志和数据刷盘。
innodb_fast_shutdown = 2。这个表示关闭的时候仅仅日志刷盘其他什么都不做就好像MySQL crash了一样。
这个参数值越大MySQL关闭的速度越快但是启动速度越慢相当于把关闭时候需要做的工作挪到了崩溃恢复上。另外如果MySQL要升级建议使用第一种方式进行一次干净的shutdown。</p>
<p>**innodb_force_recovery: **
这个参数主要用来控制InnoDB启动时候做哪些工作数值越大做的工作越少启动也更加容易但是数据不一致的风险也越大。当MySQL因为某些不可控的原因不能启动时可以设置这个参数从1开始逐步递增知道MySQL启动然后使用SELECT INTO OUTFILE把数据导出尽最大的努力减少数据丢失。
innodb_force_recovery = 0。这个是默认的参数启动的时候会做所有的事情包括redo日志应用undo日志回滚启动后台master和purge线程ibuf合并。检测到了数据页损坏了如果是系统表空间的则会crash用户表空间的则打错误日志。
innodb_force_recovery = 1。如果检测到数据页损坏了不会crash也不会报错(<code>buf_page_io_complete</code>),启动的时候也不会校验表空间第一个数据页的正确性(<code>fil_check_first_page</code>),表空间无法访问也继续做崩溃恢复(<code>fil_open_single_table_tablespace</code><code>fil_load_single_table_tablespace</code>)ddl操作不能进行(<code>check_if_supported_inplace_alter</code>),同时数据库也被不能进行写入操作(<code>row_insert_for_mysql</code><code>row_update_for_mysql</code>等)所有的prepare事务也会被回滚(<code>trx_resurrect_insert</code><code>trx_resurrect_update_in_prepared_state</code>)。这个选项还是很常用的数据页可能是因为磁盘坏了而损坏了设置为1能保证数据库正常启动。
innodb_force_recovery = 2。除了设置1之后的操作不会运行后台的master和purge线程就不会启动了(<code>srv_master_thread</code><code>srv_purge_coordinator_thread</code>等),当你发现数据库因为这两个线程的原因而无法启动时,可以设置。
innodb_force_recovery = 3。除了设置2之后的操作不会运行undo回滚数据库也不会进行但是回滚段依然会被扫描undo链表也依然会被创建(<code>trx_sys_init_at_db_start</code>)。srv_read_only_mode会被打开。
innodb_force_recovery = 4。除了设置3之后的操作不会运行ibuf的操作也不会运行(<code>ibuf_merge_or_delete_for_page</code>),表信息统计的线程也不会运行(因为一个坏的索引页会导致数据库崩溃)(<code>info_low</code><code>dict_stats_update</code>等)。从这个选项开始,之后的所有选项,都会损坏数据,慎重使用。
innodb_force_recovery = 5。除了设置4之后的操作不会运行回滚段也不会被扫描(<code>recv_recovery_rollback_active</code>)undo链表也不会被创建这个主要用在undo日志被写坏的情况下。
innodb_force_recovery = 6。除了设置5之后的操作不会运行数据库前滚操作也不会进行包括解析和应用(<code>recv_recovery_from_checkpoint_start_func</code>)。</p>
<h2>总结</h2>
<p>InnoDB实现了一套完善的崩溃恢复机制保证在任何状态下(包括在崩溃恢复状态下)数据库挂了都能正常恢复这个是与文件系统最大的差别。此外崩溃恢复通过redo日志这种物理日志来应用数据页的方法给MySQL Replication带来了新的思路备库是否可以通过类似应用redo日志的方式来同步数据呢阿里云RDS MySQL团队在后续的产品中给大家带来了类似的特性敬请期待。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/文章/MySQL · 引擎特性 · InnoDB 数据页解析.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/文章/MySQL · 引擎特性 · 临时表那些事儿.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997fddde0a8b66","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>