1、Redo log
(1)Redo log的目的
目的:redo log让MySQL有崩溃恢复的能力
在Mysql,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程IO成本、查找成本都很高,为了解决这个问题,MySQL的设计者就采用了WAL技术,Write-Ahead Logging,它的关键点就是先写日志,再写磁盘,具体来说,当有一条记录需要更新的时候,InnoDB引擎就会先把记录写到redo log里面,并更新内存,这个时候更新就算完成了。
同时InnoDB引擎会在适当的时候,将这个操作记录更新到磁盘里面。而这个更新往往是在系统比较空闲的时候做的。
InnoDB的redo log是固定大小的,比如可以配置为一组4个文件,每个文件的大小是1GB,那么这块“粉板”总共可以记录4GB的操作,从头开始写,写到末尾就又回到开头循环写
write pos是当前记录的位置,一边写一边后移,写到第3号文件末尾后就回到0号文件开头。
checkpoint是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
wirte pos和checkpoint之间的“粉板”上还空着的部分,可以用来记录新的操作。如果write pos追上checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下俩先擦掉一些记录,把checkpoint推进一下。
有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为crash-safe。
(2)万一脏页还没得及刷新到磁盘中,MySQL就挂了,怎么办呢?
对于业务代码来说,方才执行的事务是OK的,甚至前端都接受到了请求成功的响应。那结果修改的数据没同步回磁盘,MySQL宕机了会不会导致真实数据和逻辑上的数据不一致呢?
其实不会的!
MySQL使用redo log解决了这个问题,redo故名思义:重做。
当发生事务(增、删、改)时会导致缓存页变成脏页,于此同时MySQL会将事务涉及到的: 对 XXX表空间中的XXX数据页XXX偏移量的地方做了XXX更新。
所以MySQL以外宕机重启也没关系。只要在重启时解析redo log中的事务然后重放一遍。将Buffer Pool中的缓存页重做成脏页。后续再在合适的时机将该脏页刷新到磁盘即可。
(3)redo log block
首先你得知道,redo 并不是一条条写入磁盘中去的。
在MySQL的设定中,redolog是按块,一块块的写入到磁盘中去的。
你可以类比一个数据是按页为单位来组织的,就更容易理解为啥redo log要按照block来组织redo。 本质上就是两个字:优化
log block长成下面这这样:分成Header、Body、Trailer三部分 总共512字节。而且是覆盖写入。
我粗略解读一下这幅脑图
首先既然MySQL会写redo log,说明你的sql会对缓存页造成修改,也就意味着会走MySQL设定的事物那一套机制。既然是MySQL事物,大概率就是一组增、删、改。如果每个增、删、改都会有一个对应的redo log的话,那也就是说你的事物会产生好多redolog。这些redo会先被持续不断的写入到log block中,同一个事物产生的redo log会被标记为一个redo log group。
(4) redo log buffer
了解redo log block之后,白日梦还要跟你介绍一下 redo log buffer。
这个redo log buffer 中会划分出多个rodo log block。redo log buffer 占用一块连续的内存空间,默认大小16MB。且MySQL允许我们通过参数innodb_log_buffer_size
动态的调整它。增大它的大小可以让MySQL处理大事物是不必写入磁盘。进而提升写IO性能。
引入rodo log buffer之后,就可以勾勒出这样一副脑图。
如图,产生的redo log 先写入redo log block。然后redo log block其实就在redo log buffer 中。
看到这里不知道你有没有想到这样一个问题:redo log buffer再怎么神奇毕竟也是仅仅在内存中,此时万一MySQL宕机了怎么办?redolog-buffer中的数据丢失了怎么办?毕竟没有写到磁盘上,MySQL重启后100%没办法将其恢复出来。
其实你并不用担心这种情况!
因为在MySQL的设定中,当你要Commit事务时,redolog才会持久化进磁盘,既然你没有commit,碰巧MySQL又宕机了。那让MySQL正常重启就好了啊,反正你没有commit,MySQL也也没有必要帮你恢复什么。
那 redo log buffer 何时写入磁盘呢?
- 事物提交时把它对应的那些redo log写入到磁盘中去(这个动作可由相关参数控制,下文会说)
- 当redo log buffer 使用量达到了参数
innndb_log_buffer_size
的一半时,会触发落盘。 - 会有一个后台线程,每隔1秒就会将redo log block刷新到磁盘文件中去。
- MySQL关闭时也会将其落盘。
(5)redo log的刷盘时机
承接上面描述的场景:事务提交时,率先将redo log持久化进磁盘
那你如何控制MySQL,让MySQL在Commit事务时率先将redo log持久化呢?
MySQL提供了参数innodb_flush_log_at_trx_commit
该参数有几个选项:0、1、2
- 想要保证ACID四大特性推荐设置为1:表示当你commit时,MySQL必须将rodolog-buffer中的数据刷新进磁盘中。确保只要commit是成功的,磁盘上就得有对应的rodolog日志。这也是最安全的情况。
- 设置为0:每秒写一次日志并将其刷新到磁盘。
设置为2:表示当你commit时,将redolog-buffer中的数据刷新进OS Cache中,然后依托于操作系统每秒刷新一次的机制将数据同步到磁盘中,也存在丢失的风险。
(6)推荐参数
始终设置 innodb_flush_log_at_trx_commit=1
- 如果启用了二进制日志记录,请设置
sync_binlog=1
。
这也是大家常用的双1设置,前者保证redolog的不丢失、后者保证了binlog的不丢失。
2、Bin log
Bin log让MySQL有搭建集群、数据备份、恢复数据的能力
binlog 是Mysql的二进制日志文件
(1)binlog有什么用?
如果说redolog中记录的是偏向物理层面的记录,如:对那个数据页的那个记录做了什么修改。
那么binlog中记录的是偏向逻辑层面的记录:如:对xxx表中的id=yyy的行做了什么修改,更改后的值是什么?
binlog不会记录你的select、show这类的操作。
常见的binlog有如下作用:
- delete没加where条件?不慌!binlog可以帮你恢复数据
- 搭建一套一主两从的MySQL集群,binlog帮你完成主从的数据同步。
- 审计,通过分析binlog可以排查是否存在SQL注入攻击。
默认binlog是不开启的。因为开启binlog后会稍微降低一点mysql的性能(1%)。
但是开启binlog后你就可以搭建MySQL集群,排查SQL注入,恢复误删的数据。所以线上的MySQL集群都是开启binlog的,是不是感觉开启binlog很有保障!很香呢?
**
(2)刷盘机制
其实binlog写入磁盘的机制由参数sync_binlog
控制。
- 策略1:
sync_binlog = 0
当设置sync_binlog = 0
时,表示innodb不会主动控制将binlog落盘,innodb仅仅会将binlog写入到OS Cache中,至于什么时间将binlog刷入磁盘中完全依赖于操作系统。选这种策略,一旦操作系统宕机,OS Cache中的binlog就会丢失。 - 策略2:
sync_binlog = 1
设置sync_binlog = 1
时,表示事物commit时将binlog落盘!这样哪怕机器宕机了,也能确保binlog会被写入到磁盘中。 - 策略3:
sync_binlog=N
这里的N不是0,也不是1。
当N大于1时,表示开启组提交,也就是group commit,如果你之前不层了解组提交的话,你可以这样理解它:比如N=5,那MySQL就会等收集5个binlog后再将这5个binlog一口气同步到磁盘上。好处很明显,一次IO可以往磁盘上刷入N个binlog,IO效率会有所提升。坏处也很明显,比如N=5,那当MySQL收集了4个binlog时,服务器宕机,这4个binlog就会丢失。
推荐策略
线上环境中,推荐将日志的刷盘策略设置成下图这这样。
这是也官方推荐的配置策略。这两个参数前面都已经提到过了,想必为什么这么设置,你已经很清楚了!
**
3、两种日志的不同点
1、Redo log是 InnoDB引擎特有的,binlog是Mysql的Server层实现的,所有引擎都可以使用。
2、redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1”。
3、redo log是循环写的,空间固定会用完,binlog是可以追加写的。“追加写”是指binlog文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
4、两阶段提交
InnoDB引擎在执行这个简单的update语句时的内部流程。
1、执行器先找引擎取ID=2这一行,ID是主键,引擎直接用树搜索找到这一行,如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
2、执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,在调用引擎接口写入这行新数据。
3、引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,此时redo log 处于Prepare状态,然后告知执行器执行完成了,随时可以提交事务。
4、执行器生成这个操作的binlog,并把binlog写入磁盘。
5、执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改成提交(commit)状态,更新完成。
你可能注意到了,最后三步看上去有点“绕”,将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是”两阶段提交”。
两阶段提交
**
为什么必须有“两阶段提交”呢?这是为了让两份日志之间的逻辑一致。要说明这个问题,我们得从文章开头的那个问题说起:怎样让数据库恢复到半个月内任意一秒的状态?
前面我们说过了,binlog 会记录所有的逻辑操作,并且是采用“追加写”的形式。如果你的 DBA 承诺说半个月内可以恢复,那么备份系统中一定会保存最近半个月的所有 binlog,同时系统会定期做整库备份。这里的“定期”取决于系统的重要性,可以是一天一备,也可以是一周一备。
当需要恢复到指定的某一秒时,比如某天下午两点发现中午十二点有一次误删表,需要找回数据,那你可以这么做:
- 首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;
- 然后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到中午误删表之前的那个时刻。
这样你的临时库就跟误删之前的线上库一样了,然后你可以把表数据从临时库取出来,按需要恢复到线上库去。
好了,说完了数据恢复过程,我们回来说说,为什么日志需要“两阶段提交”。这里不妨用反证法来进行解释。
由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。我们看看这两种方式会有什么问题。
仍然用前面的 update 语句来做例子。假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?
1、先写redo log后写bin log。假设在redo log写完,binlog还没有写完的时候,MySQL进程异常重启。redo log写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行c的值是1。
但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不同。
2、先写binlog 后写redo log. 如果在binlog写完之后crash,由于redo log还没写,崩溃恢复以后这个事务无效,所以这一行c的值是0,但是binlog里面已经记录了“把c从0改成1”这个日志。所以,在之后用binlog来恢复的时候就多了一个事务出来,恢复出来的这一行c的值就是1,与原库的值不同。
个人理解两阶段提交:
redo log刷盘 然后进入Prepare状态,然后执行器再写binlog,然后binlog再刷到磁盘,这就是commit阶段。**
Mysql的执行顺序