redo日志对于事务提交后,数据绝对不会丢失的意义
我们知道,在更新完Buffer Pool中的缓存页之后,必须要写一条redo log,这样才能记录下来我们对数据库做的修改。
redo log可以保证我们事务提交之后,如果事务中的增删改SQL语句更新的缓存页还没刷到磁盘上去,此时MySQL宕机了,那么MySQL重启过后,就可以把redo log重做一遍,恢复出来事务当时更新的缓存页,然后再把缓存页刷到磁盘就可以了。
redo log本质是保证事务提交之后,修改的数据绝对不会丢失的。
首先我们都知道,执行增删改SQL语句的时候,都是针对一个表中的某些数据去执行的,此时的话,首先必须找到这个表对应的表空间,然后找到表空间对应的磁盘文件,接着从磁盘文件里把你要更新的那批数据所在的数据页从磁盘读取出来,放到Buffer Pool的缓存页里去。接着实际上你的增删改SQL语句就会针对Buffer Pool中的缓存页去执行你的更新逻辑,比如插入一行数据,或者更新一行数据,或者是删除一行数据。
其实你更新缓存页的时候,会更新free链表、flush链表、lru链表,然后有专门的后台IO线程,不定时的根据flush链表、lru链表,会把你更新过的缓存页刷新回磁盘文件的数据页里去。所以大家都知道这个机制里最大的漏洞就在于,万一你一个事务里有增删改SQL更新了缓存页,然后事务提交了,结果万一你还没来得及让IO线程把缓存页刷新到磁盘文件里,此时MySQL宕机了,然后内存数据丢失,你事务更新的数据就丢失了!
但是也不可能每次你事务一提交,就把你事务更新的缓存页都刷新回磁盘文件里去,因为大家之前也都知道,缓存页刷新到磁盘文件里,是随机磁盘读写,性能是相当的差!这会导致你数据库性能和并发能力都很弱的!所以此时才会引入一个redo log机制,这个机制就是说,你提交事务的时候,绝对是保证把你对缓存页做的修改以日志的形式,写入到redo log日志文件里去的。这种日志大致的格式如下:对表空间XX中的数据页XX中的偏移量为XXXX的地方更新了数据XXX。
只要你事务提交的时候保证你做的修改以日志形式写入redo log日志,那么哪怕你此时突然宕机了,也没关系!因为你MySQL重启之后,把你之前事务更新过做的修改根据redo log在Buffer Pool里重做一遍就可以了,就可以恢复出来当时你事务对缓存页做的修改,然后找时机再把缓存页刷入磁盘文件里去。
那么有人会问了,你事务提交的时候把修改过的缓存页都刷入磁盘,跟你事务提交的时候把你做的修改的redo log都写入日志文件,他们不都是写磁盘么?差别在哪里?实际上,如果你把修改过的缓存页都刷入磁盘,这首先缓存页一个就是16kb,数据比较大,刷入磁盘比较耗时,而且你可能就修改了缓存页里的几个字节的数据,难道也把完整的缓存页刷入磁盘吗?而且你缓存页刷入磁盘是随机写磁盘,性能是很差的,因为他一个缓存页对应的位置可能在磁盘文件的一个随机位置,比如偏移量为45336这个地方。
但是如果是写redo log,第一个一行redo log可能就占据几十个字节,就包含表空间号、数据页号、磁盘文件偏移量、更新值,这个写入磁盘速度很快。此外,redo log写日志,是顺序写入磁盘文件,每次都是追加到磁盘文件末尾去,速度也是很快的。所以你提交事务的时候,用redo log的形式记录下来你做的修改,性能会远远超过刷缓存页的方式,这也可以让你的数据库的并发能力更强。
写入日志文件的redo log长什么样?
redo log里本质上记录的就是在对某个表空间的某个数据页的某个偏移量的地方修改了几个字节的值,具体修改的值是什么,他里面需要记录的就是表空间号+数据页号+偏移量+修改几个字节的值+具体的值。
所以根据你修改了数据页里的几个字节的值,redo log就划分为了不同的类型,MLOG_1BYTE类型的日志指的就是修改了1个字节的值,MLOG_2BYTE类型的日志指的就是修改了2个字节的值,以此类推,还有修改了4个字节的值的日志类型,修改了8个字节的值的日志类型。当然,如果你要是一下子修改了一大串的值,类型就是MLOG_WRITE_STRING,就是代表你一下子在那个数据页的某个偏移量的位置插入或者修改了一大串的值。
所以其实一条redo log看起来大致的结构如下所示:
日志类型(就是类似MLOG_1BYTE之类的),表空间ID,数据页号,数据页中的偏移量,具体修改的数据
大致就是一条redo log中依次排列上述的一些东西,这条redo log表达的语义就很明确了,他的类型是什么,类型就告诉了你他这次增删改操作修改了多少字节的数据;然后在哪个表空间里操作的,这个就是跟你SQL在哪个表里执行的是对应的;接着就是在这个表空间的哪个数据页里执行的,在数据页的哪个偏移量开始执行的,具体更新的数据是哪些呢。有了上述信息,就可以精准完美的还原出来一次数据增删改操作做的变动了。只不过如果是MLOG_WRITE_STRING类型的日志,因为不知道具体修改了多少字节的数据,所以其实会多一个修改数据长度,就告诉你他这次修改了多少字节的数据,如下所示他的格式:
日志类型(就是类似MLOG_1BYTE之类的),表空间ID,数据页号,数据页中的偏移量,修改数据长度,具体修改的数据
redo log是直接一条一条写入文件的吗?
平时我们执行CRUD的时候,从磁盘加载数据页到buffer pool的缓存页里去,然后对缓存页执行增删改,同时还会写redo log到日志文件里去,后续不定时把缓存页刷回磁盘文件里去,大概就是这个原理。
redo log就是一条一条的直接就写入到磁盘上的日志文件里去了吗?显然不是的!
其实MySQL内有另外一个数据结构,叫做redo log block,大概你可以理解为,平时我们的数据不是存放在数据页了的么,用一页一页的数据页来存放数据。那么对于redo log也不是单行单行的写入日志文件的,他是用一个redo log block来存放多个单行日志的。一个redo log block是512字节,这个redo log block的512字节分为3个部分,一个是12字节的header块头,一个是496字节的body块体,一个是4字节的trailer块尾。
在这里面,12字节的header头又分为了4个部分。
包括4个字节的block no,就是块唯一编号;2个字节的data length,就是block里写入了多少字节数据;2个字节的first record group。这个是说每个事务都会有多个redo log,是一个redo log group,即一组redo log。那么在这个block里的第一组redo log的偏移量,就是这2个字节存储的;4个字节的checkpoint on。
所以其实对于我们的redo log而言,他确实是不停的追加写入到redo log磁盘文件里去的,但是其实每一个redo log都是写入到文件里的一个redo log block里去的,一个block最多放496自己的redo log日志。
此时可能有人会有疑问了,到底一个一个的redo log block在日志文件里是怎么存在的?那么一条一条的redo log又是如何写入日志文件里的redo log block里去的呢?假设你有一个redo log日志文件,平时我们往里面写数据,你大致可以认为是从第一行开始,从左往右写,可能会有很多行,那么所以现在既然如此,假设你要写第一个redo log了,是不是应该起码是先在内存里把这个redo log给弄到一个redo log block数据结构里去?然后似乎你应该是等内存里的一个redo log block的512字节都满了,再一次性把这个redo log block写入磁盘文件?然后其实按照我们所说的,一个redo log block就是512字节,那么是不是真正写入的时候,把这个redo log block的512字节的数据,就写入到redo log文件里去就可以了?那么redo log文件里就多了一个block。
redo log buffer中的缓冲日志,到底什么时候可以写入磁盘
redo log buffer的缓冲机制,redo log在写的时候,都是一个事务里的一组redo log,先暂存在一个地方,完事儿了以后把一组redo log写入redo log buffer。写入redo log buffer的时候,是写入里面提前划分好的一个一个的redo log block的,选择有空闲空间的redo log block去写入,然后redo log block写满之后,其实会在某个时机刷入到磁盘里去。那么到底redo log buffer里的redo log block什么时候可以刷入到磁盘文件里去呢?另外,磁盘上到底有几个redo log日志文件?不可能大量的redo log日志都放一个文件里吧?磁盘空间会占用的越来越多吗?
首先,我们先来看看redo log block是哪些时候会刷入到磁盘文件里去:
①如果写入redo log buffer的日志已经占据了redo log buffer总容量的一半了,也就是超过了8MB的redo log在缓冲里了,此时就会把他们刷入到磁盘文件里去。
②一个事务提交的时候,必须把他的那些redo log所在的redo log block都刷入到磁盘文件里去,只有这样,当事务提交之后,他修改的数据绝对不会丢失,因为redo log里有重做日志,随时可以恢复事务做的修改。
③后台线程定时刷新,有一个后台线程每隔1秒就会把redo log buffer里的redo log block刷到磁盘文件里去。
④MySQL关闭的时候,redo log block都会刷入到磁盘里去。
忽略上面的第四条不说,因为关闭MySQL的时候必然会刷redo log到磁盘,其他三条其实我们都看到了,也就是说,如果你瞬间执行大量的高并发的SQL语句,1秒内就产生了超过8MB的redo log,此时占据了redo log buffer一半的空间了,必然会直接把你的redo log刷入磁盘里去。
上面这种redo log刷盘的情况,在MySQL承载高并发请求的时候比较常见,比如每秒执行上万个增删改SQL语句,每个SQL产生的redo log假设有几百个字节,此时却是会在瞬间生成超过8MB的redo log日志,必然会触发立马刷新redo log到磁盘。
其次,第二种情况,其实就是平时执行一个事务,这个事务一般都是在几十毫秒到几百毫秒执行完毕的,说实在的,一般正常性能情况下,MySQL单事务性能一般不会超过1秒,否则数据库操作就太慢了。那么如果在几十毫秒,或者几百毫秒的时候,执行完毕了一个事务,此时必然会立马把这个事务的redo log都刷入磁盘。
第一种情况其实是不常见的,第二种情况是比较常见的,往往redo log刷盘都是以一个短事务提交时候发生的,第三种情况就是后台线程每秒自动刷新redo log到磁盘去,这个就是说假设没有别的情况触发,后台线程自己都会不停的刷新redo log到磁盘。但是不管怎么说,主要是保证一个事务执行的时候,redo log都进入redo log buffer,提交事务的时候,事务对应的redo log必须是刷入磁盘文件,接着才算是事务提交成功,否则事务提交就是失败,保证这一点,就能确保事务提交之后,数据不会丢,有redo log在磁盘里就行了。
当然,绝对保证数据不丢,还得配置一个参数,提交事务把redo log刷入磁盘文件的os cache之后,还得强行从os cache刷入物理磁盘。最后说一下redo log日志文件的问题,我们都知道平时不停的执行增删改,那么MySQL会不停的产生大量的redo log写入日志文件,那么日志文件就用一个写入全部的redo log?对磁盘占用空间越来越大怎么办?
这些问题都可以解决,实际上默认情况下,redo log都会写入一个目录中的文件里,这个目录可以通过show variables like ‘datadir’来查看,可以通过innodb_log_group_home_dir参数来设置这个目录的。然后redo log是有多个的,写满了一个就会写下一个redo log,而且可以限制redo log文件的数量,通过innodb_log_file_size可以指定每个redo log文件的大小,默认是48MB,通过innodb_log_files_in_group可以指定日志文件的数量,默认就2个。所以默认情况下,目录里就两个日志文件,分别为ib_logfile0和ib_logfile1,每个48MB,最多就这2个日志文件,就是先写第一个,写满了写第二个。那么如果第二个也写满了呢?别担心,继续写第一个,覆盖第一个日志文件里原来的redo log就可以了。
所以最多这个redo log,mysql就给你保留了最近的96MB的redo log而已,不过这其实已经很多了,毕竟redo log真的很小,一条通常就几个字节到几十个字节不等,96MB足够你存储上百万条redo log了!如果你还想保留更多的redo log,其实调节上述两个参数就可以了,比如每个redo log文件是96MB,最多保留100个redo log文件。
如果事务执行到一半要回滚怎么办?
在执行增删改操作时候的redo log的重做日志原理,其实说白了,就是你对buffer pool里的缓存页执行增删改操作的时候,必须要写对应的redo log记录下来你做了哪些修改,redo log都是先进入redo log buffer中的一个block,然后事务提交的时候就会刷入磁盘文件里去。
这样万一要是你提交事务了,结果事务修改的缓存页还没来得及刷入磁盘上的数据文件,此时你MySQL关闭了或者是宕机了,那么buffer pool里被事务修改过的数据就全部都丢失了!但是只要有redo log,你重启MySQL之后完全是可以把那些修改了缓存页,但是缓存页还没来得及刷入磁盘的事务,他们所对应的redo log都加载出来,在buffer pool的缓存页里重做一遍,就可以保证事务提交之后,修改的数据绝对不会丢!
假设现在我们一个事务里要执行一些增删改的操作,那么必然是先把对应的数据页从磁盘加载出来放buffer pool的缓存页里,然后在缓存页里执行一通增删改,同时记录redo log日志,对吧?但是现在问题来了,万一要是一个事务里的一通增删改操作执行到了一半,结果就回滚事务了呢?比如一个事务里有4个增删改操作,结果目前为止已经执行了2个增删改SQL了,已经更新了一些buffer pool里的数据了,但是还有2个增删改SQL的逻辑还没执行,此时事务要回滚了怎么办?
这个时候就很尴尬了,如果你要回滚事务的话,那么必须要把已经在buffer pool的缓存页里执行的增删改操作给回滚了。但是怎么回滚呢?毕竟无论是插入,还是更新,还是删除,该做的都已经做了啊!所以在执行事务的时候,才必须引入另外一种日志,就是undo log回滚日志。
这个回滚日志,他记录的东西其实非常简单,比如你要是在缓存页里执行了一个insert语句,那么此时你在undo log日志里,对这个操作记录的回滚日志就必须是有一个主键和一个对应的delete操作,要能让你把这次insert操作给回退了。那么比如说你要是执行的是delete语句,那么起码你要把你删除的那条数据记录下来,如果要回滚,就应该执行一个insert操作把那条数据插入回去。如果你要是执行的是update语句,那么起码你要把你更新之前的那个值记录下来,回滚的时候重新update一下,把你之前更新前的旧值给他更新回去。如果你要是执行的是select语句呢?不好意思,select语句压根儿没有在buffer pool里执行任何修改,所以根本不需要undo log!
其实你在执行事务期间,你除了写redo log日志还必须要写undo log日志,这个undo log日志是至关重要的,没有他,你根本都没办法回滚事务!
INSRET语句的undo log回滚日志长什么样?
undo log回滚日志的作用,就是你执行事务的时候,里面很多INSERT、UPDATE和DELETE语句都在更新缓存页里的数据,但是万一事务回滚,你必须有每条SQL语句对应的undo log回滚日志,根据回滚日志去恢复缓存页里被更新的数据。
INSERT语句的undo log的类型是TRX_UNDO_INSERT_REC,这个undo log里包含了以下一些东西:
这条日志的开始位置;主键的各列长度和值;表id;undo log日志编号;undo log日志类型;这条日志的结束位置;
万一要是你现在在buffer pool的一个缓存页里插入了一条数据了,执行了insert语句,然后你写了一条上面的那种undo log,现在事务要是回滚了,你直接就把这条insert语句的undo log拿出来。然后在undo log里就知道在哪个表里插入的数据,主键是什么,直接定位到那个表和主键对应的缓存页,从里面删除掉之前insert语句插入进去的数据就可以了,这样就可以实现事务回滚的效果了!
