Mysql为什么要用两个日志模块?为什么采用二段式提交?
首先回答第一个问题,得了解Mysql的日志模块,再开始讲日志模块,先回顾一下Mysql查询一条语句的一个执行过程:
Mysql语句执行流程Mysql主要有两个日志模块:redo log
和bin log
,其中redo log
就是重做日志,
redo log(InnoDB特有的日志,发生在引擎层)
redo log是物理日志,记录了这个数据页 “做了什么改动”,发生在InnoDB引擎中,当修改数据时,InnoDB将数据写入内存,在写入redo log就算修改完了,而且redo log是顺序写磁盘速度很快。避免了修改数据时直接修改数据行带来的大量随机IO,采用了随机IO转顺序IO的策略。
redo log顺序写实际上是循环写固定几个文件,写满一轮就要从头开始覆盖。它包括两个位点,check point和write pos,write pos是写到那个位置了,循环往后递增,check point是当前要擦除的位置。二者中间的空间是可写入的,当write pos追上check point时,就会先停下更新,覆盖掉一些记录,然后继续写入redo log。
redo log 用于保证 crash-safe 能力。innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数我建议你设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。
binlog(归档日志,Server层日志)
binlog是逻辑日志,记录了“id=2的记录的c加上1”,用于归档,它又是什么用呢?顾名思义它的主要作用就是归档(还有主从)!有三种模式:
-
Statement: 记录每一条除了查询之外语句。
-
Row: 记录每一行记录修改的形式,也就是记录了哪一行改了,改了啥!就比如你update了100条记录,那它就会记录这100条记录改了啥(5.1.5版本才有)。
-
Mixed:就是statement和row的混合了,由mysql来判断这条语句用哪种形式记录!(5.1.8版本才有)
binlog没有固定大小,每次都是追加记录不会覆盖之前的。
在server层,每个存储引擎都可以使用.
最开始mysql没有InnoDB,也就没有redo log
,也就没有crash-safe能力,binlog
只能用于归档,当InnoDB以插件的形式加入mysql后,需要crash-safe能力,所以就带来了redo log
。
二者很重要的不同:
- redo log是物理日志,记录的是哪个数据页做了什么修改;binlog是逻辑日志,记录的是语句的原始逻辑,比如“id=5的c字段加1”
- redo log是循环写的,binlog是可以追加写入的,写到一定大小会写下一个,不会覆盖以前的日志
sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数我也建议你设置成 1,这样可以保证 MySQL 异常重启之后 binlog 不丢失。
综上,这个地方为什么要有两份日志,一个原始是当时MySQL自带的引擎是MyISAM,但是MyISAM没有crash-safe的能力,binlog日志只能用来归档,而InnoDB是第三方公司以插件的形式引入MySQL的,因为只依靠binlog是没有crash-safe的能力,所以要使用InnoDB的日志系统redo log。
两种日志主要的不同:
- redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用
- redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”
- redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
再谈谈为什么需要采用二段式提交,再解答这个问题之前,我们先来看看一条更新语句的执行流程:
image过程文字描述:
- 客户端发来update t set c = c + 1 where id = 2的update语句
- 经过前面的分析优化流程后,执行器调用存储引擎拿到id=2的记录
- 拿到后进行运算字段c得到新数据行
- 调用执行器将数据行写入
- 存储引擎将数据行更新到内存中,并写redo log,并且redo log的状态为prepare,返回到执行器
- 执行器根据语句生成binlog,并写入磁盘
- 执行器调用提交事务接口,将刚刚的redo log状态设置为commit,更新就完成了
可以看到这里redo log的prepare -> commit是二阶段提交事务,现在就可以回答为什么使用两阶段提交了。
为什么要使用二阶段提交呢?
redo log
和binlog
都可以保证事务的提交状态,二阶段提交可以使这两个状态在逻辑上保持一致。
两阶段提交在mysql中:
-
执行器调用存储引擎更新数据行
-
存储引擎将数据记入内存中,并写
redo log
,并将redo log
状态设置为prepare
,并返回 -
执行器生成
binlog
,并写入磁盘 -
执行器调用存储引擎的提交事务接口
-
存储引擎将刚刚的
redo log
状态设置为commit
当在以下阶段发生crash,然后用binlog
进行主备库同步:
-
完成2后,还未写binlog,所以该条修改不会写到备库里,redo log为prepare状态,在崩溃恢复时发现该事务并没有完成,会回滚,主库和备库都没有该条修改,保持了一致性。
-
完成3后,写完binlog,该条修改会同步到备库里,redo log仍为prepare状态,在崩溃恢复时发现redo log有prepare状态并且binlog里也存在该条事务并完整,会提交事务,主库和备库都会有该条修改,保持了一致性。
注:崩溃恢复的判断规则
-
如果redo log里面的事务是完整的,也就是有了commit标识,则提交事务
-
如果redo log里面的事务的prepare是完整的,但没有commit,则判断binlog里是否存在并完整
-
如果是,则提交事务
-
如果否,则回滚事务
-
这里的判断binlog是否完整是通过“binlog是有完整格式”做到的,完整格式包括:
-
statement格式的最后会有COMMIT
-
raw格式的最后会有xid event
并且在5.6.2版本后增加了binlog-checksum参数
这里redo log和binlog是通过叫一个xid的共同的数据字段做到的,在崩溃恢复的时候,会按顺序扫描 redo log:
-
如果碰到既有 prepare、又有 commit 的 redo log,就直接提交;
-
如果碰到只有 parepare、而没有 commit 的 redo log,就拿着 XID 去 binlog 找对应的事务。
正常运行的实例,最终落盘的修改数据是从redo log过去的还是从buffer pool过去的?
redo log内并没有记录完整的数据页内容,所以它不具有更新完整数据页的能力,所以最终落盘的数据不是从redo log过去的。
-
如果是正常运行的实例,数据页被修改后,与磁盘的数据页不一致,成为脏页。最终落盘的数据就是将内存中的数据页落盘。
-
在崩溃恢复场景中,InnoDB 如果判断到一个数据页可能在崩溃恢复的时候丢失了更新,就会将它读到内存,然后让 redo log 更新内存内容。更新完成后,内存页变成脏页,就回到了第一种情况的状态。
参考:
https://blog.csdn.net/qq_35362055/article/details/105707788
https://blog.csdn.net/weixin_42195978/article/details/113635093
网友评论