🚀 Database
1、数据库基础
1.1 事务的概念和特性
1.2 锁
1.3 锁协议
1.4 事务日志
1.5 MVCC实现原理
1.6 基础知识
1.6.1 三范式
1.6.2 多表连接方式
1.6.3 存储过程
1.6.4 TRUNCATE和DROP的区别
1.6.5 触发器
1.6.6 视图
2、MySQL
2.1 索引
2.2 索引组织表
2.3 InnoDB和MyISAM的区别
2.4 Checkpoint技术
2.5 宕机恢复原理
2.6 数据库优化
2.7 分库分表
2.8 一致性哈希算法
2.9 主从复制
3、Redis
3.1 概述
3.1.1 为什么Redis单线程还这么快
3.1.2 Redis数据类型
3.1.3 持久化机制
3.1.4 过期机制和内存淘汰策略
3.2 线程模型
3.3 分布式问题
3.3.1 Redis实现分布式锁
3.4 缓存异常
3.4.1 缓存击穿、缓存雪崩
3.5 高可用
3.5.1 主从复制
3.5.2 哨兵模式
3.5.3 集群模式
-
+
游客
注册
登录
宕机恢复原理
## 1 前言 MySQL 保证数据不丢失的能力主要体现在两个方面: 1. 能够**恢复到任何时间点的状态**。 1. 对于这一点,只要**保留足够的 Binlog**,就可以**通过重跑 Binlog 来实现**。 2. 能够**保证 MySQL 在任何时间段突然崩溃**,**重启之后之前提交的记录都不会丢失**。 1. 对于这一点,也就是本文所说的**宕机恢复**,即在 InnoDB 存储引擎中,**事务提交过程中任何阶段**,**MySQL 突然崩溃**,**重启后都能保证事务的完整性**,**已提交的事务不会丢失**,**未提交完整的数据会自动进行回滚**,这个能力依赖的就是[Redo Log](https://notebook.ricear.com/project-37/doc-740/#2-Redo-Log)和[Undo Log](https://notebook.ricear.com/project-37/doc-740/#3-Undo-Log)两个日志。 2. 因为宕机恢复主要体现在事务执行过程中突然崩溃,重启后能保证事务的完整性,所以在讲解具体的原理前,先了解一下 MySQL 事务执行有哪些关键阶段,后面才能依据这几个阶段来进行解析,下面以一条更新语句的执行流程为例来进行说明:![图片](/media/202107/2021-07-01_173540.png) 1. 从内存中找出这条数据记录,对其进行更新。 2. 将旧数据记录到 Undo Log 中。 3. 将对数据页的更改记录到 Redo Log Buffer 中,状态为 `prepare` 状态。 4. 将逻辑操作记录到 Binlog Cache 中。 5. 将 Redo Log 中的更改记录设置为 `commit` 状态。 > 对于内存中的数据和日志,都是由后台线程来进行处理,当触发到落盘规则后再异步进行刷盘。 ## 2 WAL 技术 1. MySQL**更改数据**的时候,**不是直接写磁盘文件中的数据**,因为**直接写磁盘文件是随机写**,**开销大性能低**,**没办法满足 MySQL 的性能要求**,因此会设计成**先在内存中对数据进行更改**,再**异步落盘**。 2. 但是**内存总是不可靠**,万一**断电重启**,**还没来得及落盘的内存数据就会丢失**,所以还需要**加上写日志这个步骤**,万一**断电重启**,还能**通过日志中的记录进行恢复**。 3. **写日志**虽然也是写磁盘,但是他**是顺序写**,相比随机写**开销更小**,能**提升语句的执行性能**。 4. 这个技术就是大多数存储系统都会用的**WAL**(Write Ahead Log)技术,也称为**日志先行**的技术,指的是**对数据文件进行修改前**,**必须将修改先记录日志**,这样可以**保证数据的一致性和持久性**,也能**提升语句执行性能**。 ## 3 核心日志模块 更新 SQL 执行过程中,总共涉及 MySQL 日志模块其中的三个核心日志,分别是 Redo Log(重做日志)、Undo Log(回滚日志)、Binlog(归档日志),下面将会对的三个日志进行一个概要介绍,详细的内容可以参考[1.4 事务日志](https://notebook.ricear.com/project-37/doc-740)。 ### 3.1 Redo Log 1. Redo Log 也称为重做日志,由**InnoDB 存储引擎层产生**,**记录的是数据库中每个页的修改**,而**不是某一行或某几行修改成怎样**,可以**用来恢复提交后的物理数据页**(**恢复数据页只能恢复到最后一次提交的位置**,因为**后面的修改恢复改之前的**)。 2. 前面提到的[WAL 技术](#2-WAL 技术),**Redo Log 就是 WAL 的典型应用**,MySQL 在**有事务提交对数据进行更改**时,只会**在内存中修改对应的数据页和 Redo Log 日志**,**完成后即表示事务提交成功**,至于**磁盘数据文件的更新**,则**由后台线程异步处理**。 3. 由于 Redo Log 的加入,**保证了 MySQL 数据一致性和持久性**(即使数据刷盘之前 MySQL 崩溃了,重启后仍然能通过 Redo Log 里的更改记录进行重放,重新刷盘),此外,还能**提升语句的执行性能**(写 Redo Log 是**顺序写**,相比于更新数据文件的随机写,日志的写入**开销更小**,能**显著提升语句的执行性能**,**提高并发量**),由此可见 Redo Log 是必不可少的。 4. Redo Log 是**固定大小**的,所以只能**循环写**,从头开始写,写到末尾就又回到开头,相当于一个环形,当**日志写满了**,就**需要对旧的记录进行擦除**,但**在擦除之前**,**需要确保这些要被擦除记录对应在内存中的数据页都已经刷到磁盘了**,**在 Redo Log 满了到擦除旧记录腾出新空间这段时间**,是**不能再接收新的更新请求**,所以**有可能会导致 MySQL 卡顿**,所以**针对并发量大的系统**,**适当设置 Redo Log 的文件大小非常重要**。 ### 3.2 Undo Log 1. Undo Log 主要提供了**回滚** 和**[多行版本控制](https://notebook.ricear.com/project-37/doc-744)**(MVCC,保证事务的原子性)两个作用。 2. 在**数据修改的流程中**,**会记录一条与当前操作相反的逻辑日志到 Undo Log 中**(可以认为当 `delete` 一条记录时,Undo Log 中会记录一条对应的 `insert` 记录,反之亦然,当 `update` 一条记录时,他记录一条对应相反的 `update` 记录),如果**因为某些原因导致事务异常失败了**,**可以借助该 Undo Log 进行回滚**,**保证事务的完整性**,所以 Undo Log 也必不可少。 ### 3.3 Binlog 1. Binlog**在 MySQL 的 Server 层产生**,**不属于任何引擎**,**主要记录用户对数据库操作的 SQL 语句**(**除了查询语句**)。 2. 之所以将 Binlog 称为归档日志,是因为**Binlog 不会像 Redo Log 一样擦掉之前的记录循环写**,**而是一直记录**,等到**超过有效期才会被清理**,**如果超过单日志的最大值**(默认 1G,可以通过变量 `mac_binlog_size` 设置),则**会新起一个文件继续记录**,但**由于日志可能是基于事务来记录的**(如 InnoDB 表类型),而**事务是绝不可能也不应该跨文件记录的**,**如果正好 Binlog 日志文件达到了最大值但事务还没有提交**,**则不会切换新的文件记录**,**而是继续增大日志**,所以 `max_binlog_size`**指定的值和实际的 Binlog 日志大小不一定相等**。 3. 正是由于**Binlog 有归档的作用**,所以 Binlog**主要用于主从同步和数据库基于时间点的还原**。 4. Binlog 是否可以简化掉,需要分场景来看: 1. 如果是**主从模式**,**Binlog 是必须的**,因为**从库的数据同步依赖的就是 Binlog**。 2. 如果是**单机模式**,并且**不考虑数据库基于时间点的还原**,**Binlog 就不是必须的**,因为**有 Redo Log 就可以保证宕机恢复的能力了**,但是**万一需要回滚到某个时间点的状态**,**这个时候就无能为力了**,所以**建议 Binlog 还是一直开启**。 ## 4 两阶段提交 1. 从上面可以看出,因为**Redo Log 影响主库的数据**,**Binlog 影响从库的数据**,所以**Redo Log 和 Binlog 必须保持一致才能保证主从数据一致**,**这是前提**。 2. 这里的 Redo Log 和 Binlog 其实就是很典型的**分布式事务场景**,因为两者本身就是两个独立的个体,要想保持一致,就必须使用分布式事务的解决方案来处理,而将 Redo Log 分成了两步,其实就是使用了**两阶段提交协议**(Two-phase Commit,2PC)。 3. 下面对更新语句的执行流程进行简化,看一下 MySQL 的两阶段提交是如何实现的:![图片](/media/202107/2021-07-02_104033.png) 1. 从图中可以看出,**事务的提交有两个阶段**,就是**将 Redo Log 的写入拆成了两个步骤**:`prepare`**和**`commit`,**中间再穿插写入 Binlog**。 2. 有时候我们也很疑惑,为什么一定要用两阶段提交呢,如果不用两阶段提交会出现什么情况,比如先写 Redo Log,再写 Binlog,或者先写 Binlog,再写 Redo Log 不行吗,下面我们用反证法来进行论证,我们继续用 `update T set c = c + 1 where id = 2` 这个例子,假设 `id = 2` 这一条数据的 `c` 初始值为 0: 1. 假如在**Redo Log 写完**,**Binlog 还没有写完**的时候,**MySQL 进程异常重启**,**由于 Redo Log 已经写完了**,**系统重启会通过 Redo Log 将数据恢复回来**,**所以恢复后这一行 `c` 的值是 1**,但是**由于 Binlog 没写完就 `crash` 了**,这时候**Binlog 里面就没有记录这个语句**,因此,**不管是现在的从库还是之后通过这份 Binlog 还原临时库都没有这一次更新**,`c`**的值还是 0**,**与原库不同**,这就**造成了主从不一致**。 2. 同理,**如果先写 Binlog**,**再写 Redo Log**,**中途系统 `crash` 了**,**也会导致主从不一致**,这里就不再详述了。 3. 所以**将 Redo Log 分成两步写**,即**两阶段提交**,**才能保证 Redo Log 和 Binlog 内容一致**,**从而保证主从数据一致**。 4. 两阶段提交虽然能**保证但事务两个日志的内容一致**,但**在多事务的情况下**,却**不能保证两者的提交顺序一致**,比如下面这个例子,假设现在有 3 个事务同时提交: ``` T1 (--prepare--binlog---------------------commit) T2 (-----prepare-----binlog----commit) T3 (--------prepare-------binlog------commit) ``` 此时各个阶段写入的顺序如下: 1. **Redo Log Prepare 的顺序:** T1 --》T2 --》T3。 2. **Binlog 的写入顺序:** T1 --》T2 --》T3。 3. **Redo Log Commit 的顺序:** T2 --》T3 --> T1。 **由于 Binlog 写入的顺序和 Redo Log 提交结束的顺序不一致**,**导致 Binlog 和 Redo Log 所记录的事务提交结束的顺序不一样**,**最终导致的结果就是主从数据不一致**。 5. 因此,在两阶段提交的流程基础上,还需要**加一个锁来保证提交的原子性**,从而**保证多事务的情况下**,**两个日志的提交顺序一致:** 1. 在早期的 MySQL 版本中,通过使用 `prepare_commit_mutex` 锁来**保证事务提交的顺序**。 2. 在**一个事务获取到锁时才能进入 `prepare`**,**一直到 `commit` 结束才能释放锁**,**下个事务才可以继续进行 `prepare` 操作**。 3. 加锁**虽然完美地解决了顺序一致性的问题**,但是又会导致另外两个新的问题: 1. **在并发量较大的时候**,**会导致对锁的争用**,**性能不佳**。 2. **每个事务提交都会进行两次 `fsync`**(写磁盘),**一次是 Redo Log 落盘**,**另一次是 Binlog 落盘**,而**写磁盘是很昂贵的操作**,对于普通磁盘,每秒的 QPS 大概也就是几百。 ## 5 组提交 1. 针对通过**在两阶段提交中加锁控制事务提交顺序这种实现方式遇到的性能瓶颈问题**可以**通过组提交的方式来解决**。 2. 在 MySQL 5.6 就引入了**Binlog 组提交**,即**BLGC**(Binary Log Group Commit)。 3. Binlog 组提交的基本思想是**引入队列机制保证 InnoDB 事务提交顺序与 Binlog 落盘顺序一致**,并**将事务分组**,**组内的 Binlog 刷盘动作交给一个事务进行**,**实现组提交目的**,具体如下图所示:![图片](/media/202107/2021-07-02_115128.png) 1. **第一阶段**(`prepare` 阶段)**:** 1. **持有**`prepare_commit_mutex`,**然后 Redo Log 到磁盘**,****设置为** `prepare`**状态**** ,**完成后就释放**`prepare_commit_mutex`,******Binlog 不作任何操作**** **。**** 2. **第二阶段**(`commit` 阶段),这里**拆分成了三步**,**每一步的任务分配给一个专门的线程处理:** 1. **Flush Stage**(写入 Binlog 缓存): 1. **持有**`lock_log_mutex`(leader 持有,follower 等待)。 2. **获取队列中的一组 Binlog**(队列中的所有事务)。 3. **写入 Binlog 缓存**。 2. **Sync Stage**(将 Binlog 落盘): 1. **释放 `lock_log_mutex`**,**持有 `lock_sync_mutex`**(leader 持有,follower 等待)。 2. **将一组 Binlog 落盘**(`fsync` 动作,最耗时,假设 `sync_binlog` 为 1)。 3. **Commit Stage**(InnoDB Commit,清除 Undo 信息): 1. **释放 `lock_sync_mutex`**,**持有 `lock_commit_mutex`**(leader 持有,follower 等待)。 2. **遍历队列中的事务**,**逐一进行 InnoDB Commit**。 3. **释放 `lock_commit_mutex`**。 4. **每个 Stage 都有自己的队列**,**队列中的第一个事务称为 leader**,**其他事务称为 follower**,**leader 控制着 follower 的行为**。 5. **每个队列各自有 `mutex` 保护**,**队列之间是顺序的**,**只有 `flush` 完成后**,**才能进入到 `sync` 阶段的队列中**,**`sync` 完成后**,**才能进入到 `commit` 阶段的队列中**,**但是这三个阶段的作业是可以同时并发执行的**,即**当一组事务在进行 `commit` 阶段时**,**其他新事务可以进行 `flush` 阶段**,**实现真正意义上的组提交**,**大幅度降低磁盘的 IOPS 消耗**。 6. **组提交虽然在每个队列中仍然保留了 `prepare_commit_mutex` 锁**,**但是锁的粒度变小了**,**变成了原来两阶段提交的 $\frac14$**,**所以锁的争用性也会大大降低**,**另外**,**组提交是批量刷盘**,**相比之前的单条记录刷盘**,**大幅度降低了磁盘的 IO 消耗**,因此**组提交比两阶段提交加锁性能更好**。 ## 6 数据恢复流程 ### 6.1 整体流程 MySQL 重启后,恢复数据的流程如下图所示: ![图片](/media/202107/2021-07-02_140848.png) 1. 首先会**检查 Redo Log 中是完整并且处于 `prepare` 状态的事务**。 2. 然后**根据 XID**(事务 ID)**从 Binlog 中找到对应事务:** 1. **如果找不到**,则**根据 Undo Log 进行回滚**。 2. **如果找到并且事务完整**,则**重新设置 Redo Log 的 `commit` 标识**,**完成事务的提交**。 ### 6.2 各阶段 MySQL 崩溃的恢复策略 1. **时刻 A**(**刚在内存中更改完数据页**,**还没有开始写 Redo Log 的时候崩溃**): 1. 因为**内存中的脏页还没刷盘**,也**没有写 Redo Log 和 Binlog**,即**这个事务还没有开始提交**,所以**崩溃恢复跟该事务没有关系**。 2. **时刻 B**(**正在写 Redo Log**,**或者已经写完 Redo Log 并且落盘后**,**处于 `prepare` 状态**,**还没有开始写 Binlog 的时候崩溃**): 1. **恢复后判断 Redo Log 的事务是不是完整的:** 1. **如果不是**,则**根据 Undo Log 回滚**。 2. **如果是完整的**,**并且是 `prepare` 状态**,则**进一步判断对应事务的 Binlog 是不是完整的:** 1. **如果不是**,则**根据 Undo Log 回滚**。 2. **如果是完整的**,则**重新设置 Redo Log 的 `commit` 标识**,**完成事务的提交**。 3. **时刻 C**(**正在写 Binlog**,**或者已经写完 Binlog 并且落盘了**,**还没有开始 `commit`Redo Log 的时候崩溃**): 1. 恢复后跟时刻 B 一样,**按照时刻 B 的处理方式进行处理**即可。 4. **时刻 D**(**正在 `commit`Redo Log 或者事务已经提交完的时候**,**还没有反馈成功给客户端的时候崩溃**): 1. 恢复后跟时刻 C 基本一样,都会**对照 Redo Log 和 Binlog 的事务完整性**,**来确认是回滚还是重新提交**。 ## 参考文献 1. [MySQL 的 crash-safe 原理解析](https://mp.weixin.qq.com/s/5i9wmJs4_Er7RaYfNnETyA)。
ricear
2021年12月19日 15:28
©
BY-NC-ND(4.0)
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码