1. 前言
一条更新语句在执行过程中不仅仅要操作不仅仅是通过数据库的组件(分析器、优化器、执行器、存储引擎等)操作表数据还涉及以下内容:
-
要操作日志系统的redo log、binlog和undo log,
-
更新操作也不是实时更新到磁盘的而是通过3.Write-Ahead Logging机制先写日志后面再将数据写磁盘的
-
写
rdoo log
日志 和bin log
日志的顺序又涉及到4.二阶段提交。 -
如果数据涉及到索引还要对5.change buffer进行操作
2. MySQL日志系统
在MySQL中,有三种日志。分别是redo log、binlog和undo log。在更新过程中要对日志进行更新(其中undo log
主要用于协助回滚,和redo log
和binlog
的联系不大,可以单独讨论)。
重启后,undo log也还是需要的,会丢失吗?所以undo log的保存策略还是很重要的
做数据恢复的操作会记录到undo log中吗?
2.1 redo log(重做日志)
Write-Ahead Logging机制
在数据更新过程中,如果每次的更新操作都需要写进磁盘,IO成本很高,故MySQL采用 先将数据写到内存中,在适当的时候将数据刷新到磁盘 的方式提高SQL操作的性能。
但是只写内存如果遇到数据库宕机就可能造成数据丢失(内存数据不能持久化),MySQL使用WAL技术(全称Write-Ahead Logging)解决内存数据可能丢失和数据更新IO成本高的问题。WAL的关键点是先写日志再写磁盘。具体来说,当有一条记录需要更新的时候,InnoDB引擎就会先把记录写到redo log里面并更新内存,InnoDB引擎会在适当的时候将这个数据刷新到磁盘里面。
redo log的结构
InnoDB的 redo log
是固定大小的,比如可以配置一组4个文件,每个文件大小是1G。那么redo log总共可以记录4GB的操作,从头开始写,写到末尾又回到开头循环写。??当写满时擦除一部分记录。存储的是物理逻辑(xxxx页修改了xxx)。
哪些日志是可以删除的?
redo log要想代替真实数据实现数据持久化,肯定也要写磁盘,也需要IO,为什么说优化了SQL操作的性能?
-
在了解redo log的结构和写log的规则后知道,redo log写磁盘是顺序写的,数据的更新时随机写的,顺序写不需要寻址移动等操作,性能优。
-
redo log 组合提交
2.2 binlog(归档日志)
为什么有两份日志?
binlog是Server层的日志,redo log是InnoDB引擎特有的日志。最开始MySQL里并没有InnoDB引擎,MySQL自带的引擎是MyISAM,但是MyISAM没有crash-safe的能力, binlog日志只能用于归档。 而InnoDB是另一个公司以插件形式引入MySQL的, 既然只依靠binlog是没有crash-safe能力的, 所以InnoDB使用另外一套日志系统——也就是redo log来实现crash-safe能力。
redo log 和 binlog的区别
-
存储的内容:可以这样理解,
binlog
记载的是update/delete/insert
这样的SQL语句,而redo log
记载的是物理修改的内容(xxxx页修改了xxx)。所以在搜索资料的时候也会有这样的说法:binlog
记录的是数据的逻辑变化,redo log
记录的是数据的物理变化 -
功能:
-
redo log
的作用是实现持久化。数据库更新写完内存中的数据,如果数据库挂了,那我们可以通过redo log
来恢复内存还没来得及刷到磁盘的数据,将redo log
加载到内存里边,那内存就能恢复到挂掉之前的数据了。 -
binlog
的作用是进行复制和恢复。主从服务器需要保持数据的一致性,通过binlog
来同步数据。如果整个数据库的数据都被删除了,binlog
存储着所有的数据变更情况,那么可以通过binlog
来对数据进行恢复。
-
-
载体:redo log是InnoDB引擎特有的。binlog是MySQL的Server层实现的,所有引擎都可以使用。(也因此MyISAM是没有crash-safe能力的)
-
记录方式:redo log是循环写的,空间固定会用完;binlog是追加写入的,一个文件写满后会切换到下一个文件而不会去覆盖以前的日志。
3.3 undo log
undo log
主要有两个作用:回滚和多版本并发控制(MVCC)
在数据修改的时候,不仅记录了redo log
,还记录undo log
,如果因为某些原因导致事务失败或回滚了,可以用undo log
进行回滚undo log
主要存储的也是逻辑日志,比如我们要insert
一条数据了,那undo log
会记录的一条对应的delete
日志。我们要update
一条记录时,它会记录一条对应相反的update记录。
回滚的实现就是找到undo log中对应的相反操作语句执行。 而多版本并发控制则是利用undo log做版本的回退(聊MVCCC时再具体讲)
3. 数据更新过程
首先是MySQL的各个功能模块,在MySQL查询过程会详细介绍。
以更新ID为2的行的某个值+1为例:
-
数据库连接,然后通过分析器进行词法分析和语法分析,再经过优化器选择索引等必要步骤。
-
取数据:执行器先找引擎取ID=2这一行。 ID是主键, 引擎直接用树搜索找到这一行。 如果ID=2这一行所在的数据页本来就在内存中, 就直接返回给执行器; 否则, 需要先从磁盘读入内存, 然后再返回。
-
更新数据到内存:执行器拿到引擎给的行数据, 把这个值加上1, 比如原来是N, 现在就是N+1, 得到新的一行数据, 再调用引擎接口写入这行新数据到内存(使用change buffer则不会马上更新到磁盘)。
-
写prepare状态redo log:将这个更新操作记录到redo log里面, 此时redo log处于prepare状态。 然后告知执行器执行完成了, 随时可以提交事务。
-
写binlog:执行器生成这个操作的binlog, 并把binlog写入磁盘。
-
修改redo log状态为commit:执行器调用引擎的提交事务接口, 引擎把刚刚写入的redo log改成提交(commit) 状态, 更新完成。
上图中,浅色框表示是在InnoDB内部执行的, 深色框表示是在执行器中执行的。
4. 二阶段提交
上图执行过程中,redo log分为prepare阶段和commit阶段,在写入binlog的前后执行,这就是二阶段提交。
4.1 为什么需要二阶段提交
为什么必须有“两阶段提交”呢? 这是为了让 redo log
和 binlog
日志的数据逻辑保持一致(两阶段提交是跨系统维持数据逻辑一致性时常用的一个方案,并非MySQL日志系统的专有,日常开发中也有可能会用到。):
-
先写redo log再写binlog:主服务器正常,从服务器(或通过备份+binlog重放恢复的某时刻数据的临时库)丢失数据。
-
先写binlog再写redo log:从服务器(或通过备份+binlog重放恢复的某时刻数据的临时库)正常,主服务器丢失数据。
4.2 二阶段提交怎么解决问题
崩溃恢复时的判断规则。
-
如果redo log里面的事务是完整的, 也就是已经有了commit标识, 则直接提交;
-
如果redo log里面的事务只有完整的prepare, 则判断对应的事务binlog是否存在并完整:完整则提交事务;否则则回滚事务
上图1、2位置处发生崩溃,这个事务会怎么处理:
-
上图的①时(也就是写入redo log 处于prepare阶段之后、 写binlog之前, 发生了崩溃): 由于此时binlog还没写, redo log也还没提交, 所以崩溃恢复的时候, 这个事务会回滚。
-
时刻B发生crash对应的就是redo log中有完整prepare且binlog完整的情况:崩溃恢复过程中事务会被提交
怎么判断binlog的完整性?
-
statement 格式的 binlog,最后会有 COMMIT;
-
row 格式的 binlog,最后会有一个
XID event
另外, 在MySQL 5.6.2版本以后, 还引入了binlog-checksum参数, 用来验证binlog内容的正确性。 对于binlog日志由于磁盘原因, 可能会在日志中间出错的情况, MySQL可以通过校验checksum的结果来发现。 所以, MySQL还是有办法验证事务binlog的完整性的。
注:binlog有三种格式:statement、row和mixed(statement和row的混合)
redo log 和 binlog是怎么关联起来的?
redo log
和 bin log
有一个共同的数据字段,叫 XID。
崩溃恢复的时候,会按顺序扫描 redo log
:
-
如果碰到既有 prepare、又有 commit 的 redo log,就直接提交;
-
如果碰到只有 parepare、而没有 commit 的 redo log,就拿着 XID 去 binlog 找对应的事务。
追问3: 处于prepare阶段的redo log加上完整binlog, 重启就能恢复, MySQL为什么要这么设计?
为什么需要判断binlog完整才认为该事务有效?即为什么需要二阶段提交?
追问4: 如果这样的话, 为什么还要两阶段提交呢? 干脆先redo log写完, 再写binlog。 崩溃恢复的时候, 必须得两个日志都完整才可以。 是不是一样的逻辑?
其实, 两阶段提交是经典的分布式系统问题, 并不是MySQL独有的,是因为MySQL需要保证 binlog
和 redo log
两个日志的事务的同时提交和回滚。
两阶段提交是为了给所有人一个机会, 当每个人都说“我ok”的时候, 再一起提交。避免 redo log
提交了但 binlog
又写入失败了,此时 redo log
已经回滚不了的情况。
追问5: 不引入两个日志, 也就没有两阶段提交的必要了。 只用binlog来支持崩溃恢复, 又能支持归档, 不就可以了?
不可以,binlog没有能力恢复“数据页”,binlog记录的是逻辑操作,必须配合一份全量备份,然后重放binlog才能实现恢复。
追问6: 那能不能反过来, 只用redo log, 不要binlog?
不可以,因为生态问题,binlog不能被redo log替代。binlog被用在高可用系统的的复制、异构的数据分析下游系统获取数据
追问7: redo log一般设置多大?
redo log 太小会导致WAL机制的能力发挥不出来,每次数据都必须强行刷数据
如果是现在常见的几个TB的磁盘的话, 就不要太小气了, 直接将redo log设置为4个文 件、 每个文件1GB吧。
追问8: 正常运行中的实例, 数据写入后的最终落盘, 是从redo log更新过来的还是从buffer pool更新过来的呢?
从buffer pool更新过去的。实际上,redo log并没有记录数据页的完整数据,所以它并没有能力自己去更新磁盘数据页,它最多只能在crash崩溃时被读取到内存中,再用内存中的数据更新磁盘中的数据。
追问9: redo log buffer是什么? 是先修改内存, 还是先写redo log文件?
redo log buffer就是一块内存, 用来先存redo日志的。
-
写redo log buffer:在执行第一个insert的时候, 数据的内存被修改了, redo log buffer也写入了日志。
-
写redo log 文件:真正把志写到redo log文件(文件名是 ib_logfile+数字) , 是在执行commit语句的时候做的。
MySQL是先写数据到内存且写redo log buffer,在事务提交时只更新刷redo log文件
crash-safe能力
使用binlog恢复数据:当需要恢复到指定的某一秒时, 比如某天下午两点发现中午十二点有一次误删表, 需要找回数据, 那你可以这么做:
-
首先, 找到最近的一次全量备份, 如果你运气好, 可能就是昨天晚上的一个备份, 从这个备份恢复到临时库;
-
然后, 从备份的时间点开始, 将备份的binlog依次取出来, 重放到中午误删表之前的那个时刻。
-
这样你的临时库就跟误删之前的线上库一样了, 然后你可以把表数据从临时库取出来, 按需要恢复到线上库去。
使用 redo log
保证数据持久化:
有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe,保证了ACID特性的持久性。
当异常重启时,数据库根据磁盘数据和redo log对比保证数据正常。
??应该结合binlog一起讲恢复的完整过程
数据和日志更新的过程和时机
binlog的写入机制:
binlog的写入逻辑: 事务执行过程中, 先把日志写到binlog cache, 事务提交的时候, 再把binlog cache写到binlog文件中并清空binlog cache。
一个事务的binlog是不能被拆开的, 因此不论这个事务多大, 也要确保一次性写入。这也影响了binlog的cache写入机制:系统给binlog cache分配了一片内存, 每个线程一个,以保证每个事务都是一次性完整连续写入的。模型如下:
其中,每个线程都有自己的binlog cache,但是共用同一份binlog files,而且会涉及到write和fsync两个动作:
-
write: 指的就是指把日志写入到文件系统的page cache, 并没有把数据持久化到磁盘, 所以速度比较快。
-
fsync: 才是将数据持久化到磁盘的操作。 一般情况下, 我们认为fsync才占磁盘的IOPS
write 和fsync的时机, 是由参数sync_binlog控制的,它的取值有三个:
-
0:表示每次提交事务都只write, 不fsync;
-
1:表示每次提交事务都会执行fsync;
-
N(N>1):表示每次提交事务都write, 但累积N个事务后才fsync。
参数sync_binlog的设置:
在实际的业务场景中, 考虑到丢失日志量的可控性, 一般不建议将这个参数设成0, 比较常见的是将其设置为100~1000中的某个数值。同时,将sync_binlog设置为N的风险是: 如果主机发生异常重启, 会丢失最近N个事务的binlog日志 丢失了就不能保证持久性了啊 。
redo log的写入机制
事务在执行过程中, 生成的 redo log
会先写到 redo log buffer
,然后 write 到文件系统的page cache中,最后再 fsync 持久化到磁盘
为了控制redo log的写入策略, InnoDB提供了innodb_flush_log_at_trx_commit参数, 它有三种可能取值:
-
0: 表示每次事务提交时都只是把redo log留在redo log buffer中;
-
1:表示每次事务提交时都将redo log直接持久化到磁盘;
-
2:表示每次事务提交时都只是把redo log写到page cache。
同时InnoDB有一个后台线程, 每隔1秒, 就会把redo log buffer中的日志, 调用write写到文件系统的page cache, 然后调用fsync持久化到磁盘。(不管上面的innodb_flush_log_at_trx_commit参数设置为何值,这个后台线程都会这样执行)
被顺道持久化到磁盘的redo log怎么处理?
有三种场景会让一个没有提交的事务的redo log写入到磁盘中:
-
redo log buffer占用的空间即将达到 innodb_log_buffer_size一半的时候,后台线程会主动写盘。 注意, 由于这个事务并没有提交, 所以这个写盘动作只是write, 而没有调用fsync, 也就是只留在了文件系统的page cache。
-
并行的事务提交的时候, 顺带将这个事务的redo log buffer持久化到磁盘。 假设一个事务A执行到一半, 已经写了一些redo log到buffer中, 这时候有另外一个线程的事务B提交, 如果innodb_flush_log_at_trx_commit设置的是1, 那么按照这个参数的逻辑, 事务B要把redo log buffer里的日志全部持久化到磁盘。 这时候, 就会带上事务A在redo log buffer里的日志一起持久化到磁盘。
-
InnoDB后台线程每隔1秒, 就会把redo log buffer中的日志, 调用write写到文件系统的page cache, 然后调用fsync持久化到磁盘。
以上redo log怎么处理?redo log是有prepare状态和commit状态的,它commit状态或prepare状态+完整binlog日志的事务日志才会提交。
配置建议:双一配置
-
redo log用于保证crash-safe能力。 innodb_flush_log_at_trx_commit这个参数设置成1的时候,表示每次事务的redo log都直接持久化到磁盘。 这个参数我建议你设置成1, 这样可以保证MySQL异常重启之后数据不丢失。
-
sync_binlog这个参数设置成1的时候, 表示每次事务的binlog都持久化到磁盘。 这个参数我也建议你设置成1, 这样可以保证MySQL异常重启之后binlog不丢失。
双一配置下,一个事务完整提交前, 需要等待两次刷盘, 一次是redo log(prepare 阶段) , 一次是binlog
一天一备跟一周一备的对比。
好处是“最长恢复时间”更短。在一天一备的模式里, 最坏情况下需要应用一天的binlog。 比如, 你每天0点做一次全量备份,而要恢复出一个到昨天晚上23点的备份,一周一备最坏情况就要应用一周的binlog了。
系统的对应指标是:RTO(恢复目标时间。当然这个是有成本的, 因为更频繁全量备份需要消耗更多存储空间, 所以这个RTO是成本换来的, 就需要你根据业务重要性来评估了。
参考文章:
浅谈mysql的两阶段提交协议 MySQL实战45讲——丁奇
-
02 | 日志系统: 一条SQL更新语句是如何执行的?
-
15 | 答疑文章(一) : 日志和索引相关问题
-
23 | MySQL是怎么保证数据不丢的?