前言
在MySQL架构组件中简单介绍了MySQL的一些基本组件,本节以InnoDB为背景介绍几个核心日志模块
由于磁盘随机读写的效率很低,MySQL为了提供性能,读写不是直接操作的磁盘文件,而是在内存中开辟了一个叫做buffer pool的缓存区域,更新数据的时候会优先更新到buffer pool,之后再由I/O线程写入磁盘。同时为了InnoDB为了保证宕机不丢失buffer pool中的数据,实现crash safe,还引入了一个叫做redo log的日志模块。另外还有处于MySQL Server层的用于备份磁盘数据的bin log,用于事务回滚和MVCC的undo log等。
一、buffer pool
buffer pool作为一个缓存池,以页为单位,用于缓存数据和索引等数据,对应表空间中的页。回到MySQL组件图,一条SQL经过服务层各个组件的处理之后,最终通过执行器调用存储引擎提供的接口执行。如果是要更新一条数据,那么会先找到数据所在页,将该页加载到buffer pool中,在buffer pool中对数据进行修改,最终会通过IO线程再以页为单位将缓存中的数据刷入磁盘。
二、redo log
由于引入了buffer pool,数据不是实时写入磁盘的,如果数据还没有写入磁盘的时候MySQL宕机了,那么缓存中的数据不就丢失了吗?使用redo log来记录这些操作,即使MySQL宕机,那么在重新启动后也能根据这些记录来恢复还没有来得及写入磁盘的数据,进而保证了事务的持久性。redo log是物理日志,记录了某个数据页上做了什么修改,属于InnoDB引擎,MyISAM等不具备,下文会提到的binlog则属于服务层,底层存储引擎都共享。
有了buffer pool的介绍,我们知道记录会先在buffer pool中更新,当pool中更新之后会在redo log buffer中添加对应的记录,记录某个数据页上做了什么修改,事务会被设置为prepare状态,这个时候就可以开始根据策略刷盘了,然后等待Server层处理(比如binlog写入),在事务提交之后,标识redo log为已提交。
redo log buffer中的数据也不是直接入盘,中间还会经过操作系统内核空间的缓冲区,也就是前文图中的os buffer,然后才到磁盘上的redo log file。innodb_flush_log_at_trx_commit参数可以控制redo log buffer何时写入redo log file,该参数有三个可选值,分别如下:
- 0:延迟写。不会在事务提交时立即将redo log buffer写入到os buffer,而是每秒写入os buffer,然后立即写入到redo log file,也就是每秒刷盘
- 1:实时写,实时刷。每次事务提交都会将redo log buffer写入os buffer,然后立即写入redo log file。数据能够及时入盘,但是每次事务提交都会刷盘,效率较低
- 2:实时写,延时刷。每次事务提交都将redo log buffer写入os buffer,然后每秒将os buffer写入redo log file
redo log在内存中是由首位相连的四个文件组成的,如下图(网图):
写入的时候从文件头部开始写,每当要增加一条数据,就依次往尾部添加,所有文件写满之后又会从头开始写,之前位置的数据会被覆盖。刷盘时也是从文件头部开始读取,所以需要两个参数分别表示刷盘后的位置和写入位置。上图中,write pos表示当前写入位置,check point表示刷盘后的位置。假设redo log buffer的数据是顺时针读写,那么check point顺时针到write pos之间的数据是待刷盘数据,如果不刷盘数据则会被覆盖,write pos顺时针到check point之间则是可用的空间。
buffer pool中的数据需要刷盘,redo log buffer中的数据页也需要刷盘。如果事务提交成功之后buffer pool中的数据还没有刷盘,这时MySQL宕机了,那么在重启的时通过比对redo log file和数据页,可以从redo log file中恢复数据,redo log file根据innodb_flush_log_at_trx_commit参数配置,通常最多丢失一秒的数据。
引入redo log后,一条更新SQL的流程是这样的(二阶段提交后文说明):
三、binlog
redo log buffer主要可以在buffer pool数据还未刷盘宕机时保证事务的持久性。而binlog主要用于主从复制和数据恢复。可以通过参数max_binlog_size设置每个binlog文件的大小,新增binlog数据时直接向文件末尾添加,如果文件大小达到了参数配置值,那么数据会记录到新文件中,这个和redo log的环形日志有鲜明的对比。binlog日志有三种级别,可以通过binlog-format指定,分别是:
- statement:基于SQL语句的赋值。只记录SQL,不记录数据变更,日志文件比较小,能够节约网络和磁盘IO,但是准确性不高,对一些系统函数,比如now(),不能准确复制。
- row:基于行的变更。不记录SQL,记录每行实际数据的变更,准确性比较高,但是由于记录了数据变更,所以日志文件较大,相对于statement有更高的网络和磁盘IO,通常建议使用这个级别。
- mixed:基于statement和row的混合模式。默认使用statement,statement无法复制的操作则使用row。可能发生binlog丢失,导致主从不一致(还没去验证过)
在主从同步的场景中,Master开启了binlog日志之后,会根据binlog级别将对应的日志内容记录到二进制文件中。Slave上会启动IO线程连接到Master,请求读取指定日志文件指定位置的日志内容,Master接收到Slave的请求后,会有根据请求的日志文件和位置读取日志内容,然后返回给Slave,同时还会返回所读取日志文件现在到了哪个位置。
Slave收到日志内容后,会将数据添加到relay log文件的末尾,并且将Master返回的binlog文件和对应的最新位置记录到master info文件中,下次读取对应日志文件的时候就可以告诉Master从这个位置开始读取。Slave检测到realy log文件有新增后,会解析内容,如果日志基于statement,那么就在Slave上重新执行这些SQL,如果日志基于row,那么Slave直接根据日志内容对对应的行做修改。
但是要注意的是,对于Master来说,binlog也不是每次直接写入磁盘的,binlog也有一个binlog cache,在事务提交之前,数据会放入binlog cache,提交之后再从binlog cache刷入磁盘。通过参数binlog_cache_size可以设置binlog cache的大小,通过参数sync_binlog控制刷盘策略:
- 0:不立即刷盘,由系统决定何时将binlog cache刷盘,性能最高,但是可能会丢失多个事务的数据
- N:每N个事务提交之后,将binlog cache刷盘,当N=1时,数据最安全,最多丢失一个事务的数据,但是性能也最低;当N大于1时,会累积多个事务,类似于0的情况,可能会丢失多个事务的数据
四、两阶段提交
MySQL最开始是没有InnoDB引擎的,binlog日志位于Server层,只是用于归档和主从复制,本身不具备crash safe的能力。而InnoDB依靠redo log具备了crash safe的能力,redo log和binlog同时记录,就需要保证两者的一致性。在前面小节中已经体现了,两个log的写入流程是:
写入relo log->事务状态设置为prepare->写入binlog->提交事务->修改redolog事务状态为commit
先prepare后commit,这个称为两段提交。那么为什么需要两个段提交呢?redo log和binlog是两种不同的日志,就类似于分布式中的多节点提交请求,需要保证事务的一致性。redo log和binlog有一个公共字段XID,代表事务ID。当参数innodb_support_xa打开时,在执行事务的第一条SQL时候会去注册XA,根据第一条SQL的query id拼凑XID数据,然后存储在事务对象中。
如果两个日志单纯的分开提交,则可能会引发一些问题,如果简单分开提交,那么对于一条更新语句执行,有两种情况:
- 先写binlog,后写redo log:如果binlog写入了,在写redo log之前数据库宕机。那么在重启恢复的时候,通过binlog恢复了数据没问题。但是由于redo log没有写入,这个事务应该无效,也就是原库中就不应该有这条语句对应的更新。但是通过binlog恢复数据后,数据库中就多了这条更新
- 先写redo log,后写binlog:如果redo log写入了,在写binlog之前数据库宕机。那么在重启恢复的时候,通过binlog恢复从库,那么相对于主库来说,从库就少了这条更新
采取了两段提交之后,怎么做crash恢复呢?如果在写入binlog之前宕机了,那么事务需要回滚;如果事务commit之前宕机了,那么此时binlog cache中的数据可能还没有刷盘,那么验证binlog的完整性:到redo log中找到最近事务的XID,根据这个XID到binlog中去找(XID Event),如果找到了,说明在binlog中对应事务已经提交,那么提交redo log中事务即可;否则需要回滚事务。
至于为什么有binlog和redo log的共存,导致了这么一个复杂的局面,前面也提到了,InnoDB是后面引进MySQL的,redo log属于InnoDB特有,保证了事务的持久性,而binlog则位于Server层,用于归档。
五、undo log
当一个事务对记录做出了变更,就会就会产生undo log,默认被记录到**系统表空间(ibdata)**中,在5.6之后的版本也可以使用独立的undo 表空间。
关于表空间,可以理解为磁盘上的物理文件,比如table1.ibd,就代表的table1表的独立表空间,其中包含了数据页、索引等数据。可以通过语句:
> show variables like '%innodb_data_file_path%'
> 结果:ibdata1:12M:autoextend
查看系统表空间,结果显示格式为name:size:attributes,分别表示名称,大小和属性,autoextend表示其会随着数据增多自动扩容。
相对于redo log是一种物理日志(记录了某个数据页发生了什么更改)来说,undo log则是一种逻辑日志,当一个事务对记录做了变更操作就会产生undo log,也就是说undo log记录了记录变更的逻辑过程。笼统的说,当一个事务要更新一行记录时,会把当前记录当做历史快照保存下来,多个历史快照会用两个隐藏字段trx_id和roll_pointer串起来,形成一个历史版本链,当需要事务回滚时,可以依赖这个历史版本链将记录回滚到事务开始之前的状态,从而保证了事务的原子性(一个事务对数据库的所有操作,要么全部成功,要么全部失败)。
总结来说,在InnoDB里,undo log分为两种类型:
- insert undo log:插入产生的undo log。不需要维护历史版本链,因为没有历史数据,所以其产生的undo log可以在事务提交之后删除,不需要purge操作
- update undo log:更新或删除产生的undo log。不会在提交后就立即删除,而是会放入undo log历史版本链,用于MVCC,最后由purge线程清理
为了保证多个事务并发操作,在写各自的undo log时不产生冲突,InnoDB采用一种叫做回滚段(rollback segment,rseg)的结构来存储undo log,InnoDB中最多可以创建128个回滚段,每个回滚段维护了一个段头页,在该页中又划分了1024个slot,每个slot又对应到一个undo log对象。虽然回滚段最多可以有128个,但是对于回滚段的布局结构:
- rseg0:预留在系统表空间ibdata中
- rseg 1- rseg 32:这32个回滚段存放于临时表的系统表空间中
- rseg33 - 128: 普通回滚段,根据配置存放到独立的undo表空间中,或者如果没有打开独立undo表空间,则存放于系统表空间ibdata中
所以理论上InnoDB最多支持 96 * 1024个普通事务。在每一个读写事务开始(或只读事务转变为读写事务)的时候,都会以轮询的方式预先为其分配一个普通回滚段(对于只读事务,如果产生对临时表的写入,则需要使用第1~32号临时表回滚段为其分配回滚段),回滚段分配之后,在这个事务的生命周期内,都会使用这个回滚段。
当一个事务要存储undo log的时候,就会从这个事务所使用的回滚段的1024个slot中根据使用undo log类型(insert或update)分配一个slot,如果同一类型的slot之前已经分配过,那么可以直接使用,否则就需要分配一个slot,并创建一个对应的undo页,然后初始化。但是如果回滚段都用完了则会返回错误。
在事务提交后,需要purge的回滚段会被放到purge队列上,留待后台purge线程清理。另外还需要注意一点,对于删除操作,InnoDB并不是真正的删除原来的记录,而是将其delete mark设置为1,purge线程会把这些标记未删除的数据真正从磁盘上删除。
参考:
https://mp.weixin.qq.com/s/XTpoYW–6PTqotcC8tpF2A
http://mysql.taobao.org/monthly/2015/04/01/
https://developer.aliyun.com/article/646471
https://zhuanlan.zhihu.com/p/33504555