工作三年,小胖不知道 MySQL 日志是干嘛的。真的菜
01 前言
事情是这样的,我负责我司的报表系统,小胖是我小弟。某天他手贱误删了一条生产的数据。被用户在群里疯狂投诉质问,火急火燎的跑来问我怎么办。我特么冷汗都出来了,训斥了他一顿:蠢,蠢得都可以进博物馆了,生产的数据能随便动?
小胖看我平常笑嘻嘻的,没想到发这么大的火。心一急,居然给我跪下了:远哥,我上有老,下有小,中有女朋友,不要开除我呀。我一听火更大了:合着就你有女朋友???这个时候我们 DBA 老林来打圆场:别慌,年轻人管不住下本身,难免做错事。我可以把数据恢复到一个月内任意时刻的状态。听到这,小胖忙抱着老林大腿哭爹喊娘地感谢。
听到这你是不是很奇怪?能恢复到半个月前的数据?DBA 老林到底是如何做到的?我跟他细聊了一番。老林点燃了手中 82 年的华子,深深吸了一口说到:事情还得从 update 语句是如何执行的说起。
1.1 从更新语句说起
假设我现在有建表语句,如下:
表数据如下:
今天恰好张三生日,我要把它的 age 加一岁。于是执行以下的 sql 语句:
前面聊过查询语句是如何执行的?错过的同学看这篇《工作三年:小胖连 select 语句是如何执行的都不知道,真的菜!》,里面的查询语句流程,更新语句也会走一遍,如下流程图:
update 语句发起:首先连接器会连接数据库。接着分析器通过词法、语法分析知道这是更新语句。所以查询缓存失效。
之前的文章提到:如果表有更新。那么它的查询缓存会失败。这也是为啥,我不建议你使用查询缓存的原因。
优化器则决定使用 ID 索引去更新,最后执行器负责找到这行数据,执行更新。
重点来了:与查询流程不一样,更新还涉及两个重要的日志模块。一是重做日志: redo log,二是归档日志:binlog。要解答文章开头的问题,必须要明白这两日志的原理才能整明白 DBA 是怎么做到的。
02 事务日志:redo log
什么是 redo log?为了方便理解,先举个来自极客时间的例子:
还记得《孔乙己》这篇文章,饭店掌柜有一个粉板,专门用来记录客人的赊账记录。如果赊账的人不多,那么他可以把顾客名和账目写在板上。但如果赊账的人多了,粉板总会有记不下的时候,这个时候掌柜一定还有一个专门记录赊账的账本。
>
如果有人要赊账或者还账的话,掌柜一般有两种做法:
>
- 一种做法是直接把账本翻出来,把这次赊的账加上去或者扣除掉;
>
- 另一种做法是先在粉板上记下这次的账,等打烊以后再把账本翻出来核算。
>
在生意红火柜台很忙时,掌柜一定会选择后者,因为前者操作实在是太麻烦了。首先,你得找到这个人的赊账总额那条记录。你想想,密密麻麻几十页,掌柜要找到那个名字,可能还得带上老花镜慢慢找,找到之后再拿出算盘计算,最后再将结果写回到账本上。
>
这整个过程想想都麻烦。相比之下,还是先在粉板上记一下方便。你想想,如果掌柜没有粉板的帮助,每次记账都得翻账本,效率是不是低得让人难以忍受?
2.1 为什么需要 redo log?
同样,在 MySQL 中,如果每一次的更新要写进磁盘,这么做会带来严重的性能问题:
因为 Innodb 是以页为单位进行磁盘交互的,而一个事务很可能只修改一个数据页里面的几个字节,这时将完整的数据页刷到磁盘的话,太浪费资源了!
一个事务可能涉及修改多个数据页,并且这些数据页在物理上并不连续,使用随机 IO 写入性能太差!
为了解决这个问题,MySQL 的设计者就用了类似掌柜粉板的思路来提升更新效率。这种思路在 MySQL 中叫 WAL(Write-Ahead Logging),意思就是:先写 redo log 日志,后写磁盘。日志和磁盘就对应上面的粉板和账本。
具体到 MySQL 是这样的:有记录需要更新,InnDB 把记录写到 redo log 中,并更新内存中的数据页,此时更新就算完成。同时,后台线程会把操作记录更新异步到磁盘中的数据页。
PS:当需要更新的数据页在内存中时,就会直接更新内存中的数据页;不在内存中时,在可以使用 change buffer(篇幅有限,这个后面写文章再聊) 的情况下,就会将更新操作记录到 change buffer 中,并将这些操作记录到 redo log 中;如果此时有查询操作,则触发 merge 操作,返回更改后的记录值。
有些人说 InnoDB 引擎把日志记录写到 redo log 中,redo log 在哪,不也是在磁盘上么?
对,这也是一个写磁盘的过程,但是与更新过程不一样的是,更新过程是在磁盘上随机 IO,费时。 而写 redo log 是在磁盘上顺序 IO,效率要高。
PPS:redo log 的存在就是把全局的随机写,变换为局部的顺序写,从而提高效率。
2.2 redo log 的写入过程
redo log 记录了事务对数据页做了哪些修改。它包括两部分:分别是内存中的日志缓冲(redo log buffer)和磁盘上的日志文件(redo logfile)。
mysql 每执行一条 DML 语句,先将记录写入 redo log buffer,后续某个时间点再一次性将多个操作记录写到 redo log file。也就是我们上面提到的 WAL 技术。
计算机操作系统告诉我们:用户空间下的缓冲区数据是无法直接写入磁盘的。因为中间必须经过操作系统的内核空间缓冲区(OS Buffer)。
所以,redo log buffer 写入 redo logfile 实际上是先写入 OS Buffer,然后操作系统调用 fsync() 函数将日志刷到磁盘。过程如下:
mysql 支持三种将 redo log buffer 写入 redo log file 的时机,可以通过 innodbflushlog_at_trx_commit 参数配置,各参数值含义如下:建议设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。
| 参数值 | 含义 |
| ------ | ---- |
| 0(延迟写)|事务提交时不会将 redo log buffer 中日志写到 os buffer,而是每秒写入 os buffer 并调用 fsync() 写入到 redo logfile 中。也就是说设置为 0 时是(大约)每秒刷新写入到磁盘中的,当系统崩溃,会丢失 1 秒钟的数据。|
| 1(实时写、实时刷新)|事务每次提交都会将 redo log buffer 中的日志写入 os buffer 并调用 fsync() 刷到 redo logfile 中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO 的性能差。|
| 2(实时写、延迟刷新刷新)|每次提交都仅写入到 os buffer,然后是每秒调用 fsync() 将 os buffer 中的日志写入到 redo log file。|
写的过程如下:
2.3 redo log file 的结构
InnoDB 的 redo log 是固定大小的。比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么 redo log file 可以记录 4GB 的操作。从头开始写。写到末尾又回到开头循环写。如下图:
上图中,write pos 表示 redo log 当前记录的 LSN (逻辑序列号) 位置,一边写一遍后移,写到第 3 号文件末尾后就回到 0 号文件开头; check point 表示数据页更改记录刷盘后对应 redo log 所处的 LSN(逻辑序列号) 位置,也是往后推移并且循环的。
PS:check point 是当前要擦除的位置,它与数据页中的 LSN 应当是一致的。
write pos 到 check point 之间的部分是 redo log 的未写区域,可用于记录新的记录;check point 到 write pos 之间是 redo log 已写区域,是待刷盘的数据页更改记录。
当 write pos 追上 check point 时,表示 redo log file 写满了,这时候有就不能执行新的更新。得停下来先擦除一些记录(擦除前要先把记录刷盘),再推动 check point 向前移动,腾出位置再记录新的日志。
2.4 什么是 crash-save ?
有了 redo log ,即在 InnoDB 存储引擎中,事务提交过程中任何阶段,MySQL 突然奔溃,重启后都能保证事务的完整性,已提交的数据不会丢失,未提交完整的数据会自动进行回滚。这个能力称为 crash-safe,依赖的就是 redo log 和 undo log 两个日志。
比如:重启 innodb 时,首先会检查磁盘中数据页的 LSN ,如果数据页的 LSN 小于日志中 check point 的 LSN ,则会从 checkpoint 开始恢复。
2.5 回滚日志 undo log
undo log,主要提供回滚的作用,但还有另一个作用,就是多个行版本控制 (MVCC),保证事务的原子性。在数据修改的流程中,会记录一条与当前操作相反的逻辑日志到 undo log 中(可以认为当 delete 一条记录时,undo log 中会记录一条对应的 insert 记录,反之亦然,当 update 一条记录时,它记录一条对应相反的 update 记录),如果因为某些原因导致事务异常失败了,可以借助该 undo log 进行回滚,保证事务的完整性,所以 undo log 也必不可少。
03 归档日志:binlog
上一篇聊查询语句的执行过程时,聊到 MySQL 的架构包含 server 层和引擎层。而 redo log 是 InnoDB 引擎特有的日志,而 server 层也有自己的日志,那就是 binlog。
最开始 MySQL 里并没有 InnoDB 引擎。MySQ L 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。
3.1 binlog 日志格式?
binlog 有三种格式,分别为 STATMENT 、 ROW 和 MIXED。
在 MySQL 5.7.7 之前,默认的格式是 STATEMENT , MySQL 5.7.7 之后,默认值是 ROW。日志格式通过 binlog-format 指定。
STATMENT:每一条会修改数据的 sql 语句会记录到 binlog 中 。
ROW:不记录 sql 的上下文信息,仅需记录哪条数据被修改。记两条,更新前和更新后都有。
MIXED:前两种模式的混合,一般的复制使用 STATEMENT 模式保存 binlog ,对于 STATEMENT 模式无法复制的操作使用 ROW 模式保存 binlog
3.2 binlog 可以做 crash-save 吗?
只用一个 binlog 是否可以实现 cash_safe 能力呢?答案是可以的,只不过 binlog 中也要加入 checkpoint,数据库故障重启后,binlog checkpoint 之后的 sql 都重放一遍。但是这样做让 binlog 耦合的功能太多。
有人说,也可以直接直接对比匹配全量 binlog 和磁盘数据库文件,但这样做的话,效率低不说。因为 binlog 是 server 层的记录并不是引擎层的,有可能导致数据不一致的情况:
假如 binlog 记录了 3 条数据,正常情况引擎层也写了 3 条数据,但是此时节点宕机重启,binlog 发现有 3 条记录需要回放,所以回放 3 条记录,但是引擎层可能已经写入了 2 条数据到磁盘,只需要回放一条 1 数据。那 binlog 回放的前两条数据会不会重复呢,比如会报错 duplicate key。
另外,binlog 是追加写,crash 时不能判定 binlog 中哪些内容是已经写入到磁盘,哪些还没被写入。而 redolog 是循环写,从 check point 到 write pos 间的内容都是未写入到磁盘的。
所以,binlog 并不适合做 crash-save。
3.3 两种日志的区别
redo log 和 binlog 主要有三种不同:
redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
redo log 是物理日志,记录的是在某个数据页上做了什么修改;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如 "给 ID=2 这一行的 age 字段加 1"。
redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。追加写是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
3.4 update 语句的执行流程
了解了两种日志的概念,再来看看执行器和 InnoDB 引擎在执行 update 语句时的流程:
执行器取 id = 2 的行数据。ID 是主键,引擎用树搜索找到这一行。如果这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,再返回。
执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,redo log 会写入 binlog 的文件名和位置信息来保证 binlog 和 redo log 的一致性,更新完成。
整个过程如下图所示,其中橙色框表示是在 InnoDB 内部执行的,绿色框表示是在执行器中执行的:
3.5 两阶段提交
由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。我们看看这两种方式会有什么问题。
仍然用前面的 update 语句来做例子。假设当前 id=2 的行,字段 age 的值是 22,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?
先写 redo log 后写 binlog。假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于我们前面说过的,redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 age 的值是 22。
但是 binlog 没写完就 crash 了,这时 binlog 里面并没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。
等到需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 age 值就是 22,与原库的值不同。
先写 binlog 后写 redo log。如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以 age 的值是 22。但是 binlog 里面已经记录了"把从 22 改成 23" 这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 age 的值就是 23,与原库的值不同。
所以,如果不使用"两阶段提交",数据库的状态就有可能和用 binlog 恢复出来的不一致。
另外:sync_binlog 这个参数建议设置成 1,表示每次事务的 binlog 都持久化到磁盘,这样可以保证 MySQL 异常重启之后 binlog 不丢失。
3.6 binlog 的应用场景
主从复制 :在 Master 端开启 binlog ,然后将 binlog 发送到各个 Slave 端, Slave 端重放 binlog 从而达到主从数据一致。
数据恢复 :通过使用 mysqlbinlog 工具来恢复数据。
04 数据恢复的过程
前面说过,binlog 会记录所有的逻辑操作,并且是采用"追加写"的形式。如果你的 DBA 承诺说一个月内可以恢复,那么备份系统中一定会保存最近一个月的所有 binlog,同时系统会定期做整库备份。这里的"定期"取决于系统的重要性,可以是一天一备,也可以是一周一备。
当需要恢复到指定的某一秒时,比如某天下午两点发现中午十二点有一次误删表,需要找回数据,那你可以这么做:
首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;
然后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到中午误删表之前的那个时刻。
这样你的临时库就跟误删之前的线上库一样了,然后你可以把表数据从临时库取出来,按需要恢复到线上库。
看到这里,小胖露出了目视父亲的笑容。
巨人的肩膀
《高性能 MySQL》
zhihu.com/question/411272546/answer/1375199755
zhihu.com/question/425750274/answer/1525436152
time.geekbang.org/column/article/68633
my.oschina.net/vivotech/blog/4289724
hiddenpps.blog.csdn.net/article/details/108505371
05 总结
本文讲解了事务日志(redo og)的几个方面:为什么需要 redo log?它的写入过程、结构、存的啥以及什么是 crash-save 等等;此外还聊了 binlog 的定义、日志格式、与 redo log 的区别、update 语句的执行流程、两阶段提交、以及 binlog 的应用场景。
好啦,以上就是狗哥关于 MySQL 日志的总结。感谢各技术社区大佬们的付出,如果说我看得更远,那是因为我站在你们的肩膀上。希望这篇文章对你有帮助,我们下篇文章见~
06 送点书
如果看到这里,喜欢这篇文章的话,请帮点个好看。微信搜索一个优秀的废人,关注后回复电子书送你 1000+ 本编程电子书 ,包括 C、C++、Java、Python、GO、Linux、Git、数据库、设计模式、前端、人工智能、面试相关、数据结构与算法以及计算机基础,详情看下图。回复 1024 送你一套完整的 java 视频教程。
版权声明: 本文为 InfoQ 作者【一个优秀的废人】的原创文章。
原文链接:【http://xie.infoq.cn/article/267dd9c1151d9f95403575aca】。文章转载请联系作者。
评论