MySQL作为当下最流行的开源关系型数据库,有一个很关键和基本的能力,就是必须能够保证数据不会丢。那么在这个能力背后,MySQL是如何设计才能保证不管在什么时间崩溃,恢复后都能保证数据不会丢呢?有哪些关键技术支撑了这个能力?

文章目录

  • 前言
  • WAL机制
  • 核心日志模块
  • 两阶段提交
  • 组提交
  • 数据恢复流程
  • 本文小结

前言

MySQL 保证数据不会丢的能力主要体现在两方面:

  1. 能够恢复到任何时间点的状态;
  2. 能够保证MySQL在任何时间段突然奔溃,重启后之前提交的记录都不会丢失;

对于第一点将MySQL恢复到任何时间点的状态,相信很多人都知道,只要保留有足够的binlog,就能通过重跑binlog来实现。

对于第二点的能力,也就是本文标题所讲的crash-safe。即在 InnoDB 存储引擎中,事务提交过程中任何阶段,MySQL突然奔溃,重启后都能保证事务的完整性,已提交的数据不会丢失,未提交完整的数据会自动进行回滚。这个能力依赖的就是redo log和unod log两个日志。


因为crash-safe主要体现在事务执行过程中突然奔溃,重启后能保证事务完整性,所以在讲解具体原理之前,先了解下MySQL事务执行有哪些关键阶段,后面才能依据这几个阶段来进行解析。下面以一条更新语句的执行流程为例,话不多说,直接上图:

从上图可以清晰地看出一条更新语句在MySQL中是怎么执行的,简单进行总结一下:

  1. 从内存中找出这条数据记录,对其进行更新;
  2. 将对数据页的更改记录到redo log中;
  3. 将逻辑操作记录到binlog中;
  4. 对于内存中的数据和日志,都是由后台线程,当触发到落盘规则后再异步进行刷盘;

上面演示了一条更新语句的详细执行过程,接下来咱们通过解答问题,带着问题来剖析这个crash-safe的设计原理。


WAL机制

为什么不直接更改磁盘中的数据,而要在内存中更改,然后还需要写日志,最后再落盘这么复杂?

这个问题相信很多同学都能猜出来,MySQL更改数据的时候,之所以不直接写磁盘文件中的数据,最主要就是性能问题。因为直接写磁盘文件是随机写,开销大性能低,没办法满足MySQL的性能要求。所以才会设计成先在内存中对数据进行更改,再异步落盘。但是内存总是不可靠,万一断电重启,还没来得及落盘的内存数据就会丢失,所以还需要加上写日志这个步骤,万一断电重启,还能通过日志中的记录进行恢复。

写日志虽然也是写磁盘,但是它是顺序写,相比随机写开销更小,能提升语句执行的性能(针对顺序写为什么比随机写更快,可以比喻为你有一个本子,按照顺序一页一页写肯定比写一个字都要找到对应页写快得多)。

这个技术就是大多数存储系统基本都会用的WAL(Write Ahead Log)技术,也称为日志先行的技术,指的是对数据文件进行修改前,必须将修改先记录日志。保证了数据一致性和持久性,并且提升语句执行性能。


核心日志模块

更新SQL语句执行流程中,总共需要写3个日志,这3个是不是都需要,能不能进行简化?

更新SQL执行过程中,总共涉及MySQL日志模块其中的三个核心日志,分别是redo log(重做日志)、undo log(回滚日志)、binlog(归档日志)。这里提前预告,crash-safe的能力主要依赖的就是这三大日志。

接下来,针对每个日志将单独介绍各自的作用,然后再来评估是否能简化掉。

重做日志 redo log

redo log也称为事务日志,由InnoDB存储引擎层产生。记录的是数据库中每个页的修改,而不是某一行或某几行修改成怎样,可以用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置,因为修改会覆盖之前的)。

前面提到的WAL技术,redo log就是WAL的典型应用,MySQL在有事务提交对数据进行更改时,只会在内存中修改对应的数据页和记录redo log日志,完成后即表示事务提交成功,至于磁盘数据文件的更新则由后台线程异步处理。由于redo log的加入,保证了MySQL数据一致性和持久性(即使数据刷盘之前MySQL奔溃了,重启后仍然能通过redo log里的更改记录进行重放,重新刷盘),此外还能提升语句的执行性能(写redo log是顺序写,相比于更新数据文件的随机写,日志的写入开销更小,能显著提升语句的执行性能,提高并发量),由此可见redo log是必不可少的。

redo log是固定大小的,所以只能循环写,从头开始写,写到末尾就又回到开头,相当于一个环形。当日志写满了,就需要对旧的记录进行擦除,但在擦除之前,需要确保这些要被擦除记录对应在内存中的数据页都已经刷到磁盘中了。在redo log满了到擦除旧记录腾出新空间这段期间,是不能再接收新的更新请求,所以有可能会导致MySQL卡顿。(所以针对并发量大的系统,适当设置redo log的文件大小非常重要!!!)

回滚日志 undo log

undo log顾名思义,主要就是提供了回滚的作用,但其还有另一个主要作用,就是多个行版本控制(MVCC),保证事务的原子性。在数据修改的流程中,会记录一条与当前操作相反的逻辑日志到undo log中(可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录),如果因为某些原因导致事务异常失败了,可以借助该undo log进行回滚,保证事务的完整性,所以undo log也必不可少。

归档日志 binlog

binlog在MySQL的server层产生,不属于任何引擎,主要记录用户对数据库操作的SQL语句(除了查询语句)。之所以将binlog称为归档日志,是因为binlog不会像redo log一样擦掉之前的记录循环写,而是一直记录(超过有效期才会被清理),如果超过单日志的最大值(默认1G,可以通过变量 max_binlog_size 设置),则会新起一个文件继续记录。但由于日志可能是基于事务来记录的(如InnoDB表类型),而事务是绝对不可能也不应该跨文件记录的,如果正好binlog日志文件达到了最大值但事务还没有提交则不会切换新的文件记录,而是继续增大日志,所以 max_binlog_size 指定的值和实际的binlog日志大小不一定相等。

正是由于binlog有归档的作用,所以binlog主要用作主从同步和数据库基于时间点的还原。

那么回到刚才的问题,binlog可以简化掉吗?这里需要分场景来看:

  1. 如果是主从模式下,binlog是必须的,因为从库的数据同步依赖的就是binlog;
  2. 如果是单机模式,并且不考虑数据库基于时间点的还原,binlog就不是必须,因为有redo log就可以保证crash-safe能力了;但如果万一需要回滚到某个时间点的状态,这时候就无能为力,所以建议binlog还是一直开启;

根据上面对三个日志的详解,我们可以对这个问题进行解答:在主从模式下,三个日志都是必须的;在单机模式下,binlog可以视情况而定,保险起见最好开启


两阶段提交

为什么redo log要分两步写,中间再穿插写binlog呢?

从上面可以看出,因为redo log影响主库的数据,binlog影响从库的数据,所以redo log和binlog必须保持一致才能保证主从数据一致,这是前提。

相信很多有过开发经验的同学都知道分布式事务,这里的redo log和binlog其实就是很典型的分布式事务场景,因为两者本身就是两个独立的个体,要想保持一致,就必须使用分布式事务的解决方案来处理。而将redo log分成了两步,其实就是使用了两阶段提交协议(Two-phase Commit,2PC)。

下面对更新语句的执行流程进行简化,看一下MySQL的两阶段提交是如何实现的:


从图中可看出,事务的提交过程有两个阶段,就是将redo log的写入拆成了两个步骤:prepare和commit,中间再穿插写入binlog。


组提交

针对通过在两阶段提交中加锁控制事务提交顺序这种实现方式遇到的性能瓶颈问题,有没有更好的解决方案呢?

答案自然是有的,在MySQL 5.6 就引入了binlog组提交,即BLGC(Binary Log Group Commit)。binlog组提交的基本思想是,引入队列机制保证InnoDB commit顺序与binlog落盘顺序一致,并将事务分组,组内的binlog刷盘动作交给一个事务进行,实现组提交目的。具体如图:

第一阶段(prepare阶段):

持有prepare_commit_mutex,并且write/fsync redo log到磁盘,设置为prepared状态,完成后就释放prepare_commit_mutex,binlog不作任何操作。

第二个阶段(commit阶段):

这里拆分成了三步,每一步的任务分配给一个专门的线程处理:

1.Flush Stage(写入binlog缓存)

① 持有Lock_log mutex [leader持有,follower等待]
② 获取队列中的一组binlog(队列中的所有事务)
③ 写入binlog缓存

2.Sync Stage(将binlog落盘)

①释放Lock_log mutex,持有Lock_sync mutex[leader持有,follower等待]
②将一组binlog落盘(fsync动作,最耗时,假设sync_binlog为1)。

3.Commit Stage(InnoDB commit,清楚undo信息)
①释放Lock_sync mutex,持有Lock_commit mutex[leader持有,follower等待]
② 遍历队列中的事务,逐一进行InnoDB commit
③ 释放Lock_commit mutex

每个Stage都有自己的队列,队列中的第一个事务称为leader,其他事务称为follower,leader控制着follower的行为。每个队列各自有mutex保护,队列之间是顺序的。只有flush完成后,才能进入到sync阶段的队列中;sync完成后,才能进入到commit阶段的队列中。但是这三个阶段的作业是可以同时并发执行的,即当一组事务在进行commit阶段时,其他新事务可以进行flush阶段,实现了真正意义上的组提交,大幅度降低磁盘的IOPS消耗。

针对组提交为什么比两阶段提交加锁性能更好,简单做个总结:组提交虽然在每个队列中仍然保留了prepare_commit_mutex锁,但是锁的粒度变小了,变成了原来两阶段提交的1/4,所以锁的争用性也会大大降低;另外,组提交是批量刷盘,相比之前的单条记录都要刷盘,能大幅度降低磁盘的IO消耗。


数据恢复流程

假设事务提交过程中,MySQL进程突然奔溃,重启后是怎么保证数据不丢失的?

下图就是MySQL重启后,提供服务前会先做的事 – 恢复数据的流程:


对上图进行简单描述就是:奔溃重启后会检查redo log中是完整并且处于prepare状态的事务,然后根据XID(事务ID),从binlog中找到对应的事务,如果找不到,则回滚;找到并且事务完整则重新commit redo log,完成事务的提交。

下面我们根据事务提交流程,在不同的阶段时刻,看看MySQL突然奔溃后,按照上述流程是如何恢复数据的。

  1. 时刻A(刚在内存中更改完数据页,还没有开始写redo log的时候奔溃):
    因为内存中的脏页还没刷盘,也没有写redo log和binlog,即这个事务还没有开始提交,所以奔溃恢复跟该事务没有关系;
  2. 时刻B(正在写redo log或者已经写完redo log并且落盘后,处于prepare状态,还没有开始写binlog的时候奔溃):
    恢复后会判断redo log的事务是不是完整的,如果不是则根据undo log回滚;如果是完整的并且是prepare状态,则进一步判断对应的事务binlog是不是完整的,如果不完整则一样根据undo log进行回滚;
  3. 时刻C(正在写binlog或者已经写完binlog并且落盘了,还没有开始commit redo log的时候奔溃):
    恢复后会跟时刻B一样,先检查redo log中是完整并且处于prepare状态的事务,然后判断对应的事务binlog是不是完整的,如果不完整则一样根据undo log回滚,完整则重新commit redo log;
  4. 时刻D(正在commit redo log或者事务已经提交完的时候,还没有反馈成功给客户端的时候奔溃):
    恢复后跟时刻C基本一样,都会对照redo log和binlog的事务完整性,来确认是回滚还是重新提交。

本文小结

至此对MySQL 的crash-safe原理细节就基本讲完了,简单回顾一下:

  1. 首先简单介绍了WAL日志先行技术,包括它的定义、流程和作用。WAL是大部分数据库系统实现一致性和持久性的通用设计模式;
  2. 接着对MySQL的日志模块,redo log、undo log、binlog、两阶段提交和组提交都进行了详细介绍;
  3. 最后讲解了数据恢复流程,并从不同时刻加以验证。

MySQL的crash-safe原理详解相关推荐

  1. MySQL分区分表 原理详解

    MySQL分区分表 为什么要分区和分表 我们的数据库数据越来越大,随之而来的是单个表中数据太多,以至于查询速度过慢,而且由于表的锁机制导致应用操作也受到严重影响,出现数据库性能瓶颈. MySQL中有一 ...

  2. mysql udf提权原理_udf提权原理详解

    0x00-前言 这个udf提权复现搞了三天,终于搞出来了.网上的教程对于初学者不太友好,以至于我一直迷迷糊糊的,走了不少弯路.下面就来总结一下我的理解. 想要知道udf提权是怎么回事,首先要先知道ud ...

  3. mysql悲观锁会有脏数据吗_mysql悲观锁原理详解

    mysql中的锁概念 mysql已经成为大家日常数据存储的最常用平台,但随着业务量和访问量的上涨,会出现并发访问等场景,如果处理不好并发问题的话会带来严重困扰.下面介绍一下如何通过mysql的悲观锁来 ...

  4. MySQL索引原理详解

    MySQL索引原理详解 索引的本质 索引的分类 Hash 索引 二叉树 B树(二三树) B+树 主键目录 索引页 索引页的分层 非主键索引 回表 索引的本质 索引的本质是一种排好序的数据结构. 索引的 ...

  5. LVS原理详解(3种工作方式8种调度算法)--老男孩

    一.LVS原理详解(4种工作方式8种调度算法) 集群简介 集群就是一组独立的计算机,协同工作,对外提供服务.对客户端来说像是一台服务器提供服务. LVS在企业架构中的位置: 以上的架构只是众多企业里面 ...

  6. mybatis的原理详解

    mybatis的原理详解 原理图 执行的原理图如下图所示: 配置文件分析 config.xml: <?xml version="1.0" encoding="UTF ...

  7. LVS原理详解(3种工作模式及8种调度算法)

    2017年1月12日, 星期四 LVS原理详解(3种工作模式及8种调度算法) LVS原理详解及部署之二:LVS原理详解(3种工作方式8种调度算法) 作者:woshiliwentong  发布日期:20 ...

  8. mysql中 where in 用法详解

    https://blog.csdn.net/haibo0668/article/details/52584307 sssss mysql中 where in 用法详解 我是高手高手高高手 2016-0 ...

  9. JSPatch实现原理详解:让JS调用/替换任意OC方法

    JSPatch实现原理详解:让JS调用/替换任意OC方法 2015-07-10 09:05 编辑: suiling 分类:iOS开发 来源:bang JSPatch以小巧的体积做到了让JS调用/替换任 ...

  10. mysql极限_SQL极限函数limit()详解分页必备

    limit含义: limit英语中的含义是限制,限定的意思.小日本曾上映过一个电影就是叫limit是由漫画改编的电影,剧情很变态,但不可否认小日本由于地狭人稠的原因,在观念上的资源危机意识还是很强的哈 ...

最新文章

  1. 如果优美的将pytorch的卷积为自己所用
  2. leetcode算法题--不同的二叉搜索树 II
  3. Debug Current Instruction Pointer怎么去掉(蓝色调试箭头)
  4. Fiddler抓包一键生成调用代码
  5. linux 禁止其他用户登录,在Linux中如何禁止用户登录
  6. Hadoop Exception in createBlockOutputStream
  7. matlab简单分析信号调制解调
  8. [软件发布]可媲美EndNote的文献管理软件与论文写作辅助软件--RefNavigator
  9. 标记集合 java编译_深入理解Java虚拟机读书笔记-java编译期和运行期优化
  10. 2022年4月第十三届蓝桥杯省赛C组C语言/C++真题及答案
  11. Java 动态编译基础学习
  12. 如何压缩word文档的大小?
  13. Java链表(基本操作)
  14. python给excel排序_使用Python对Excel中的列数据进行排序
  15. 双核心三层交换机路由功能详细配置
  16. (二)基于数据库的认证与授权
  17. 算法复杂度:算法时间复杂度和空间复杂度表示法
  18. 求导: H61 集成主板 装10.15超级黑苹果
  19. 表达式求值(C语言)
  20. SAP License:金审系统与SAP接口

热门文章

  1. 自己写的.Net(C#)代码×××
  2. vi保存退出:x与:wq的区别
  3. 可能拥有海洋的天然卫星 科学家近日发现有食盐成分
  4. 9102 BITRUN Hackathon is COMING!
  5. wireshark 抓 grpc 包
  6. xpath中如何使用变量
  7. 使用tomcat8下的websocket进行web前后端通信
  8. 团队管理中的每日站立会
  9. mysql--SQL编程(关于mysql中的日期) 学习笔记2
  10. mysql 主从同步,双主同步,如果服务器意外挂机,不同步怎么办