MySQL系列2:InnoDB存储引擎
1. 架构回顾
上一篇我们讲解了MySQL的逻辑架构,重新回顾一下,用一张新的图来认识一下该架构。
整体架构分为service层与存储引擎层,请求交给连接池后,由后台线程处理,并将请求转发给SQL接口,随后交给解析器执行,如果解析器发现命中缓存,直接从缓存读数据返回,如果没有,依次往下执行,直到从存储引擎再到磁盘或者内存(存储引擎对应的缓存中)查询结果返回。
2. 三种日志
在聊存储引擎前,不得不聊三种日志,undo log、redo log、binlog,因为存储引擎的执行过程中时刻跟写日志与刷盘有关系。
2.1 undo log
undo log是做回滚用的,记录了某一次数据更新或者修改的逆向操作,比如现需要修改记录,将a=1更新成a=2,undo log就记录执行逆向操作,将a=2更新成a=1,再比如将某一条数据删除,undo log就记录改数据的恢复操作,insert该数据,保证数据操作不成功,通过undo log能恢复到修改前的版本。
2.2 redo log
redo log主要是保证数据修改不丢失。该日志属于存储引擎层,属于物理日志,记录修改了哪些数据。有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。
2.3 binlog
binlog属于service层,记录了sql功能上做的操作,属于逻辑日志,比如执行了什么样的sql更新语句等。主要用来做历史数据恢复与主从同步。
2.4 binlog与redo log区别
- redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
- redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
- redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
3. InnoDB的内存结构Buffer Pool以及执行流程
InnoDB存储引擎中有一个非常重要的放在内存里的组件,就是缓冲池(Buffer Pool),这里面会缓存很多的数据,以便于以后在查询的时候,万一你要是内存缓冲池里有数据,就可以不用去查磁盘了。我们先要明确一点事实,就是所有的操作都是基于Buffer Pool进行操作,而不是磁盘,因为Buffer Pool内存操作速度快。举个例子,引擎要更新“id=10”这一行数据 ,先看一下下图的执行流程。
- 现将该数据从磁盘加载到缓存;
- 将数据的逆向操作记录到undo log日志,保存旧值,未来如果事务未提交可以执行回滚,恢复原始数据;
- 在Buffer Pool中更新数据;
- 将Redo Log写入Redo Log Buffer;
- 执行Redo Log的
第一个阶段,也叫prepare准备阶段
,将Redo Log日志刷到对应的磁盘文件; - 执行binlog刷盘,将binlog日志也刷到对应的磁盘文件;
- 执行Redo Log的
第二个阶段,也叫commit提交阶段
,对应的binlog文件名称和这次更新的binlog日志在文件里的位置,都写入到redo log日志文件里去,同时在redo log日志文件里写入一个commit标记
。在完成这个事情之后,才算最终完成了事务的提交。
上面流程还涉及其他细节,比如刷盘策略,为何要两阶段等,将在下面一一展开。
3.1 Redo Log刷盘策略
redo log有三种刷盘策略,该策略是通过innodb_flush_log_at_trx_commit来配置的,0-不刷磁盘,1-刷磁盘(建议),2-刷os cache,下图分析如果刷os cache,默认1s以后才能刷到磁盘,期间宕机会导致数据丢失,如下图。
如果设置不刷盘,Buffer Pool清空后数据也一样丢失,所以建议设置参数为1。如果上述刷盘不成功,第一阶段事务就没成功,后续binlog就根本不会执行,整个事务都会回滚,相当于更新白做。
3.2 binlog刷盘策略
sync_binlog参数可以控制binlog的刷盘策略,他的默认值是0,此时你把binlog写入磁盘的时候,其实不是直接进入磁盘文件,而是进入os cache内存缓存。所以跟之前分析的一样,如果此时机器宕机,那么你在os cache里的binlog日志是会丢失的,我们看下图的示意。
如果要是把sync_binlog参数设置为1的话,那么此时会强制在提交事务的时候,把binlog直接写入到磁盘文件里去,那么这样提交事务之后,哪怕机器宕机,磁盘上的binlog是不会丢失的。
3.2 Redo Log的两阶段提交
当我们把binlog写入磁盘文件之后,接着就会完成最终的事务提交,此时会把本次更新对应的binlog文件名称和这次更新的binlog日志在文件里的位置,都写入到redo log日志文件里去,同时在redo log日志文件里写入一个commit标记。在完成这个事情之后,才算最终完成了事务的提交,我们看下图的示意
最后一步在redo日志中写入commit标记的意义是什么?说白了,他其实是用来保持redo log日志与binlog日志一致的。我们来举个例子,假设我们在提交事务的时候,一共有上图中的5、6、7三个步骤,必须是三个步骤都执行完毕,才算是提交了事务。那么在我们刚完成步骤5的时候,也就是redo log刚刷入磁盘文件的时候,mysql宕机了,此时怎么办?这个时候因为没有最终的事务commit标记在redo日志里,所以此次事务可以判定为不成功。不会说redo日志文件里有这次更新的日志,但是binlog日志文件里没有这次更新的日志,不会出现数据不一致的问题。
如果要是完成步骤6的时候,也就是binlog写入磁盘了,此时mysql宕机了,怎么办?同理,因为没有redo log中的最终commit标记,因此此时事务提交也是失败的。
必须是在redo log中写入最终的事务commit标记了,然后此时事务提交成功,而且redo log里有本次更新对应的日志,binlog里也有本次更新对应的日志 ,redo log和binlog完全是一致的。
下面有图来展示一下这个两阶段提交的过程
prepare 阶段
:将 XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 持久化到磁盘;
commit 阶段
:把 XID 写入到 binlog,然后将 binlog 持久化到磁盘,接着调用引擎的提交事务接口,将 redo log 状态设置为 commit,此时该状态并不需要持久化到磁盘,只需要 write 到文件系统的 page cache 中就够了,因为只要 binlog 写磁盘成功,就算 redo log 的状态还是 prepare 也没有关系,一样会被认为事务已经执行成功。
通过这种两阶段提交的方案,就能够确保redo-log、bin-log两者的日志数据是相同的。
3.3 后台IO线程随机将内存更新后的脏数据刷回磁盘
现在我们假设已经提交事务了,此时一次更新“update users set name='xxx' where id=1”,他已经把内存里的buffer pool中的缓存数据更新了,同时磁盘里有redo日志和binlog日志,都记录了把我们指定的“id=1”这行数据修改了“name='xxx'”。此时我们会思考一个问题了,但是这个时候磁盘上的数据文件里的“id=1”这行数据的name字段还是等于旧的值啊!所以MySQL有一个后台的IO线程,会在之后某个时间里,随机的把内存buffer pool中的修改后的脏数据给刷回到磁盘上的数据文件里去,我们看下图:
当上图中的线程把buffer pool里的修改后的脏数据刷回磁盘的之后,磁盘上的数据才会跟内存里一样,都是name=xxx这个修改以后的值了!在你线程把脏数据刷回磁盘之前,哪怕mysql宕机崩溃也没关系,因为重启之后,会根据redo日志恢复之前提交事务做过的修改到内存里去,就是id=1的数据的name修改为了xxx,然后等适当时机,线程自然还是会把这个修改后的数据刷到磁盘上的数据文件里去的。
4 总结
大家通过一次更新数据的流程,就可以清晰地看到,InnoDB存储引擎主要就是包含了一些buffer pool、redo log buffer等内存里的缓存数据,同时还包含了一些undo日志文件,redo日志文件等东西,同时mysql server自己还有binlog日志文件。在执行更新的时候,每条SQL语句,都会对应修改buffer pool里的缓存数据、写undo日志、写redo log buffer几个步骤;但是当你提交事务的时候,一定会把redo log刷入磁盘,binlog刷入磁盘,完成redo log中的事务commit标记;最后后台的IO线程会随机的把buffer pool里的脏数据刷入磁盘里去。