听我讲完 redo log、binlog 原理,面试官老脸一红,rabbitmq 集群同步原理
错误日志(errorlog)
慢查询日志(slow query log)
一般查询日志(general log)
中继日志(relay log)
面试官:好,那你先说一下你对 redo log
日志的理解吧。
熊猫:记得小时候看《武林外传》,吕秀才柜台下面有一个小黑板,当时不知道是干啥的,后来发现是专门用来记录客人的赊账记录。如果赊账的人不多,那么他可以把顾客名和账目写在板上。但如果赊账的人多了,小黑板没地儿了,这个时秀才一定还有一个专门记录赊账的账本。如果有人要赊账或者还账的话,秀才一般有两种做法:
一种做法是直接把账本翻出来,把这次赊的账加上去或者扣除掉;
另一种做法是先在小黑板上记下这次的账,等打烊以后再把账本翻出来核算。
在生意火爆时,秀才肯定会选择后者,因为直接记账本太麻烦了。得先翻出赊账人“老钱”那条记录,账本密密麻麻几十页,找到后再拿出算盘计算,最后更新到账本上。想想都麻烦。相比之下,还是先在小黑板上记一下方便。你想想,如果秀才没有小黑板的帮助,每次记账都得翻账本,效率是不是低得让人难以忍受?还有时间泡小郭?想无双?
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210325181823644.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0c
HM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM5MzkwNTQ1,size_16,color_FFFFFF,t_70#pic_center)
同样,在 MySQL 里也有这个问题,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。为了解决这个问题,MySQL 的设计者就用了类似秀才记小黑板的思路来提升更新效率。
而小黑板和账本配合的过程,其实就是 MySQL 里经常说到的 WAL 技术
。
面试官:小伙子你这思路很奇特呀!那你再详细跟我说一下,啥是 WAL 技术?
熊猫:(小马哥对我有意思啊!)
WAL 的全称是 Write-Ahead Logging,它的关键点就是先写日志,再写磁盘,也就是先写小黑板,等不忙的时候再写账本。
具体来说,当有一条 update 语句要执行的时候,InnoDB 引擎就会先把记录写到 redo log(小黑板)里面,并更新内存,这个时候更新就算完成了。
同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做,这就像打烊以后秀才做的事。如果今天赊账的不多,掌柜可以等打烊后再整理。但如果某天赊账的特别多,小黑板写满了咋办?这个时候秀才只好叫无双帮忙干自己的活儿,抓紧把小黑板中的一部分赊账记录更新到账本中,然后把这些记录从小黑板上擦掉,为记新账腾出空间。
与此类似,InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 100MB,那么这块“小黑板”总共就可以记录 400MB 的操作记录。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。
write position 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。
checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
write position 和 checkpoint 之间的是“小黑板”上还空着的部分,可以用来记录新的操作。
如果 write pos 追上 checkpoint,表示“小黑板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe
。
crash-safe:
??可以对照前面赊账记录的例子。只要赊账记录记在了小黑板上或写在了账本上,即使秀才突然被老邢抓走几天,回来后依然可以通过账本和小黑板上的数据明确赊账账目。就是维护数据的持久性。
??
本质上说,crash-safe 就是落盘处理,将数据存储到了磁盘上,断电重启也不会丢失。
面试官:不错,你这理解虽说听的我一愣一愣,但是话糙理不糙,确实说出了 redo log 的原理。那你再说说对 binlog 日志的理解吧。
熊猫: 嘿嘿,谢谢马经理夸奖。MySQL 其实是分为 server 层 和 引擎层两部分。
Server 层:它主要做的是 MySQL 功能层面的事情;
引擎层:负责存储相关的具体事宜。
上面我们聊到的“小黑板” redo log 是 InnoDB 引擎特有的日志
,而 Server 层也有自己的日志,称为binlog(归档日志),其实就是用来恢复数据用的。
面试官:那 MySQL 为啥要有 redo log 和 binlog 两个日志呢?只留一个不香么?
熊猫:因为最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,而 binlog 日志只用于归档。
InnoDB 是另一个公司以插件形式引入 MySQL 的。我们知道,只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。
面试官:那这两个日志主要有哪些区别?
熊猫:emmm…主要有几下几种区别:
redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎共用。
redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=1 这一行的 c 字段加 1 ”。
redo log 是循环写的,空间固定会用完然后复写;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
面试官:好,基于你上面说的,那比如下面这条 SQL,你来描述一下在 MySQL 内部的执行流程吧
update T set money = money + 500 where username = '陈哈哈';
熊猫:
(开始,原始数据接入)执行器先找引擎取 username = ‘陈哈哈’ 这一行。如果 username = ‘陈哈哈’ 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
(数据修改)执行器拿到引擎给的行数据,把 money 这字段的值加上 500,比如原来是 N,现在就是 N+500,得到新的一行数据,再调用引擎接口写入这行新数据。
(数据提交)提交操作,由于存储引擎层与 server 层之间采用的是内部 XA(保证两个事务的一致性,这里主要保证 redo log 和 binlog 的原子性),所以提交分为 prepare 阶段与 commit 阶段,也就是我们说的
两阶段提交
。(写 redo log)引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面(写到内存或直接落盘),到这里, redo log 处于 prepare 状态。
(写 binlog)然后告知执行器执行完成了,随时可以提交事务。执行器生成这个操作的 binlog,并把 binlog 同步到磁盘。
(数据更新到磁盘或内存,结束)执行器调用引擎的提交事务接口执行修改操作,需要将在二级索引上做的修改,写入到 change buffer page,等到下次有其他 sql 需要读取该二级索引时,再去与二级索引做 merge,引擎把刚刚写入的 redo log 标记上(commit)状态,实际上是加上了一个与 binlog 对应的 XID,使两个日志逻辑保持一致,到此结束,更新流程闭环。
面试官:那为啥必须要分成 prepare 和 commit 两个阶段进行提交呢?一块儿提交他不爽么。
熊猫:我举个现实生活中的栗子吧,一个完整的交易过程我认为应该这样:
比如你来我的小超市里买一瓶可乐:
小马哥:老板给我来瓶可乐!透心凉心飞扬的那个。
我:??
机器扫一下可乐,告诉小马哥这瓶可乐 2 块 5,不能白嫖,让他给钱(记录 redo log,事务处于 prepare 状态)
收钱放入钱箱(记录 binlog,
事务实际是否完成的根本依据
,处于待标记 commit 阶段)然后让你把可乐拿走(redo log 状态标为 commit,表示该事务逻辑闭环)。
到这里,代表一笔交易结束
。并告诉小马哥,透心凉心飞扬那个是雪碧,你个憨 X~
等算账前再把这一天卖东西的交易信息一起同步到数据库。
可见,如果收钱之前(prepare 阶段,步骤 3)交易被打断,回过头来处理此次交易,发现只有记了小黑板但没有收钱,则交易失败,删掉小黑板上的记录(回滚);
如果收了钱后(commit 阶段 或 待 commit 阶段,步骤 4 || 5)交易被打断,然后回过头发现系统上有记录(prepare)而且钱箱有本次收入(bin log),则说明本次交易有效,补充修改 commit 状态,更新到库存中。
以上是人话,咱们再来看看 MySQL 层面的专业解释:
这里我们用反证法来进行解释为何需要两阶段提交。由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。我们看看这两种方式会有什么问题。
仍然用前面的 update 语句来做例子。假设当前 username = ‘陈哈哈’ 的行,账户余额字段 money 的值是 100,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash(异常宕机),会出现什么情况呢?
依旧以这条 SQL 为例:
update T set money = 0 + 500 where username = '陈哈哈';
[](
)1、先写 redo log 后写 binlog。
假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于我们前面说过的,redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 money 的值是 money + 500。
但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。
然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 money 的值就是 0,与原库的值不同。
[](
)2、先写 binlog 后写 redo log。
如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,用户余额 money 的值应当是 0。但是 binlog 里面已经记录了“把 money 从 0 改成 500 这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 money 的值就是 500,与原库的值不同。
可以看到,如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。
简单说,redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。
[](
)日志落盘
保证事务成功,日志必须落盘,这样,数据库 crash 后,就不会丢失某个事务的数据了
innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这样可以保证 MySQL 异常重启之后数据不丢失。
评论