Redo log产生原因
为了提升读写性能,引入了Buffer Pool作为缓存,当要读写一个页时,该页先被加载到Buffer Pool中,对该页的读写都在内存中进行.
为了支持ACID中D,每一个事务(显式或隐式地)提交后,数据就必须持久化. 因此事务对Buffer Pool中缓存页的修改,必须在提交时同步到磁盘. 一个事务可能造成多个页的更新,如果每次提交都将这些页从缓存写入磁盘将带来以下后果:
- 即使只需要修改一个页内的少量数据,也要将整个缓存页(16KB)写入磁盘一次.
- 这些页通常不连续,从而带来的大量随机IO.
为了避免频繁地将脏页同步到磁盘,引入了redo log. redo log记录了对哪个表空间的哪个页作出何种更新,具有如下优点:
- redo log很小,写入一条redo log的成本低于同步一整个缓存页到磁盘的成本
- redo log的写入是顺序IO,成本远低于写入多个不连续页的随机IO
事务中对每一个缓存页的修改,都会记录在对应的redo log中,在事务提交时,事务过程中所产生的redo log以顺序IO的形式持久化. 如果之后服务器意外crash,则可通过redo log恢复出丢失的数据.
一言以蔽之,凭借着体积小又可物理连续IO的优势,redo log以一种轻量的方式去实现了ACID中的D.
Mini-Transaction(语句的原子性)
如果说一个事务原子性在于,事务内执行的语句要么都执行,要么都不执行. 那么单条语句的原子性就在于,其造成的若干个数据页修改,要么都生效,要么都不生效.
一条SQL语句的执行时,内部可能需要修改多个数据页,比如一条Insert语句涉及如下3个方面的数据页修改:
- 更新B+树索引节点,向表的所有索引插入记录,如果定位到的叶子页空间不够,则需要做页分裂,页分裂后,还需向父节点页面插入目录项记录(如果父节点页面的空间也不够,则又将产生页分裂)
- 更新上一条记录的next_record(为了维护页b内链表)
- 更新页内状态,比如Page Header中的页面统计信息;Page Directory中的槽信息;
一条语句在内部执行过程中,对每个页面的修改都会产生一条redo log,假如一条语句需要修改页a,b,c,产生了对应的3条redo log,之后在将redo log写入磁盘过程中,只写完2条就crash了,那么就无法完整恢复这条语句的修改,相当于一条语句只执行了一半.
为了避免上述情况的发生,InnoDB在为单条语句生成redo log时,在末尾加一条特殊的redo log MLOG_MULTI_REC_END. 假设一条语句生成了a,b,c 3条redo log,在那么会依次写入a->b->c->MLOG_MULTI_REC_END,如果写b到crash了,那么在重启后处理redo log时,由于找不到MLOG_MULTI_REC_END,就会丢弃a和b,这样就相当于这条语句没有执行过;反之如果MLOG_MULTI_REC_END写入成功,就可完整恢复这条语句的修改. 可见除了ACID中的D,redolog还保证了单条语句的原子性,我们称这一机制为Mini-Transaction(MTR).
MTR代表了单条语句所产生的redolog,一个MTR内的redolog是不可分割的,要么全部生效,要么全部不生效.
redo log的写入
redo log的结构
redo log的结构如上,记录了对哪个表空间的的哪一个页面作出何种修改,其类型有几十种,但是大致可分为如下两类:
- 直接型,从页内哪一处偏移开始,将数据替换为指定数据.
比如修改系统表空间8号表的Max Row ID时,生成的redo log,就记录在该页指定偏移处开始,写入8个字节数据.
- 间接型,记录对页作何种操作,恢复时调用特定函数处理
比如向某个页插入记录,就需要在redo log里记录,放在页内记录链表哪一处,相邻节点(记录)是谁,之后在实际处理该redo log时,特定函数中再考虑页内的其他修改,如Page Header中的页面统计信息,Page Directory中的槽信息等.
redo log block的结构
如同数据页是存放记录的容器,redo log也有存放它的容器,redo log block.一个block512KB,如同页是表空间数据磁盘读写的最小单位一样,redo log block是对redolog磁盘读写的最小单位.
redolog的缓存(redo log buffer)
引入redo log的目的是为了保证事务的持久性. 而何时持久化,则可以有不同的选择.一种选择是每生成一条redo log就写一次磁盘,这会导致IO太过频繁; 另一种更好的选择是,将事务过程中产生的redo log都缓存,当事务提交时,再将缓存的redo log一起写入磁盘.如同Buffer Pool作为页的缓存一样,redo log也有缓存,redo log buffer.
redo log buffer默认16MB,可通过
innodb_log_buffer_size设置.
如下图所示,redo log buffer中存放若干个连续的redo log block结构,每个MTR所产生的redolog被依次写入log block的body部分.
redo log的持久化(redo log file)
引入redo log buffer的目的,是为了先聚集多一点redolog,然后刷盘时可以一次性写多条.而刷盘的时机,有以下几种:
- 事务提交
- log buffer空间不足
- 后台线程定时(每秒一次)将log buffer刷新到磁盘
- 正常退出mysql进程
- checkpoint
默认情况下,redo log被依序写入数据目录的两个文件ib_logfile0,ib_logfile1,每个文件大小为48MB. 当前一个文件写满时,就往后一个文件写,当最后一个文件也写满时,就回到第一个文件写,形成一个环.
之所以可以往回写,是因为“久远的”redo log 可能已经过期, 比如一个脏页被刷盘后,之前针对该页的redo log也就没有意义了(因为整个页都已经持久化时,redo log中记录的对页修改自然也已经一同持久化了)
innodb_log_file_size指定每个redo日志文件的大小,innodb_log_file_size指定redo log日志文件的数量
尽管物理上有多个log file用于存redo log,但是逻辑上可以认为是一片连续的存储空间,我们称组成这块逻辑空间的多个文件为logfile文件组.
织数据)
与log buffer 相对应,log file也是由多个redo log block组成,前4个block用于存元信息:
- 0号block,log file header
- LOG_HEADER_START_LSN
- 1号block,checkpoint1
- LOG_CHECKPOINT_lSN,在系统crash之前,最后一次checkpoint时的lsn值
- 2号block,预留块,暂时没用
- 3号block,checkpoint2,结构同checkpoint1
每个logfile从第5个block开始(第2048个字节)的block空间用于存储redolog. 值得注意的是,一个MTR会产生多条redolog,在写入缓存或logfile时是以一个MTR为单位的,所以同一MTR的redolog在缓存中或logfile中相邻的.
redolog的回收
redo log存在的意义是,将事务对表空间中的页造成的修改记录下来并持久化,当发生crash导致脏页未来的及刷盘时,通过redo log来恢复出脏页中的修改,继而刷新磁盘页.但是crash并不常有,而redolog常有.每发生一次对页的修改,就会产生一条redolog. 一个事务内可能包含多条语句,每条语句可能修改多个页,每改一次页,都要产生一条redolog,随着时间的推移,redo log将越积越多. 而log file文件组的空间是有限的, 势必需要考虑一下redolog的“回收”问题.
概念上看,如果一个脏页被刷盘,那么之前针对该脏页的redo log都失去意义. 具体来讲,假设一个语句需要修改页abc,最初修改只作用在缓冲页abc中,随后这3个页会被放入flush链表,表示脏页待刷盘. 另一方面,在修改缓冲页abc的同时,也会产生对应的redolog abc,并放入log buffer中,之后事务提交时,buffer中redo log abc被刷入磁盘(也就是logfile).在稍后某个时间点,负责将脏页刷盘的线程,将页a刷入了磁盘,此时redolog a也就失去意义,那么logfile中redolog a所占用的空间就可以被回收. 如果随后页bc也被刷入了磁盘,那么redolog bc所占用空间也可以被回收.
如下图所示,假设缓存中有ABC三个页,MTR1修改页AC,在页AC的oldest_mtr_lsn属性记录下MTR1的的lsn 0
这里补充一下lsn的概念: lsn(log sequence number),初始值8704,buffer中每写入一个MTR(对应的redolog),就自增“这些redolog的字节个数”,用于标记多个MTR(的redolog)的先后顺序,lsn值越小说明MTR(的redolog)越早产生.
之后MTR2修改了页BC,在页B记录下MTR2的lsn 100,页C由于已经是脏页了(不是第一次修改),所以不修改oldest_mtr_lsn
在稍后某个时间点,脏页A被刷盘,此时oldest_mtr_lsn最小的是页C,值为0
在稍后某个时间点,脏页C也被刷盘,此时oldest_mtr_lsn最小的是页B,值为100
此时对MTR1来说,它所修改的页都被刷盘了,缓存中已经没有oldest_mtr_lsn属性等于MTR1的mtr_lsn的页了,此时MTR1对应的redolog就可以被回收了.
为了记录在logfile中哪些redolog已经被回收了,用一个变量checkpoint_lsn 来指向被回收的MTR的下一个MTR的mtr_lsn,如果一个MTR的mtr_lsn小于checkpoint,就意味着被回收了.
上述过程被称为checkpoint.
如何根据redo log恢复数据
在恢复数据时,面临的是logfile中redolog序列, 对于lsn值小于checkpoint_lsn的redolog,由于已经回收,这部分自然可直接跳过,所以只需从checkpoint_lsn开始处理.
恢复时面临的问题
但是,一个页刷盘,并不意味着有对应redolog被回收,还需要发生checkpoint,改变了checkpoint_lsn的值才意味着redolog被回收. 假如一个页刷盘后,没来的及checkpoint就crash了,那么就会导致logfile中存在一部分redolog,虽然它们造成的修改已经被刷盘(对应的脏页已经被刷盘了),但是它们仍未被回收. 如果在恢复数据时,将这些redolog记录的修改作用到对应的页中,就会导致对一个页的相同修改发生两次, 想象一条insert redolog,一开始数据被插入缓冲页,之后缓冲刷盘了,这条数据已经持久化道磁盘的情况下,如果再次将insert redolog执行一次,那么就向这个页插入了两条记录.
基于以上原因,即使一条redolog未被回收,也需要额外的检查,去证明这条redolog所对应的缓冲页没有刷盘,才能执行这条redolog,恢复数据到磁盘页中.
如何判断一个redolog对脏页的修改是否已经刷盘
每一个脏页在刷盘时,都会在磁盘页记录最后一个修改该页的MTR的lsn, 如果一个redolog的lsn (也就是redolog所属MTR的lsn)小于磁盘页的lsn,那么就意味着磁盘页中已经包含该, 由于后修改脏页的MTR的lsn必然大于先修改的
假设有MTR1(lsn=100),MTR2(lsn=200),MTR3(lsn=300),MTR1先修改了缓冲页A,每个页在修改时都会记录“最后一次更新该页的MTR的lsn“(称为newest_mod_lsn),所以页A会记录newest_mod_lsn=100, 之后MTR2也修改了页A,那么页A记录newest_mod_lsn=200, 之后脏页A刷盘时,会一并写入newest_mod_lsn(在File Header的FIL_PAGE_LSN字段). 这之后MTR3又修改了缓存页A,但这次在缓存页A上的修改没来的及刷盘就crash了.
对于以上情况,可作出如下分析:
- 对于MTR1中修改页A的redolog,其lsn为100,小于磁盘页A中的newest_mod_lsn, 意味着磁盘页A中已经包含redolog的修改,所以会被跳过.
- 对于MTR2中修改页A的redolog,其lsn为200,等于磁盘页A中的newest_mod_lsn, 意味着其对页A的修改刚好被刷盘了,所以会被跳过.
- 对于MTR3中修改页A的redolog,其lsn为300,其值大于磁盘页A的newest_mod_lsn, 意味crash前该修改没来的及刷盘, 所以需要从该redolog中恢复出丢失的数据.
优化恢复过程
总结一下,从logfile的checkpoint_lsn处的redolog开始,向后扫描直到最后一条,这个过程中,如果redolog的修改已经作用于磁盘中的页了,那么就忽略掉,否则执行redolog.
假如logfile中的redolog序列为: log1(修改页a)->log2(修改页b)->log3(修改页a)->log4(修改页b), 如果从前往后遍历的话,就需要发生4次随机IO, 而如果能把相同页面的redolog放到一起去写磁盘页的话,则只需要2次随机IO. 为了减少恢复时的IO成本,mysql大叔将redolog按所修改的页分组,组内的redolog按发生顺序(lsn值)排序, 遍历这样对于每个组只需要发生一次磁盘页读写就可以恢复所有针对该页的redolog.
参考文献
- 《MySql是怎样运行的》