redo log基础
重做日志用来实现事务的原子性和持久性,即事务ACID中的A和D。其由两部分组成:
一是内存中的重做日志缓冲(redo log buffer),其是易失的:
二是重做日志文件(redo log file),其是持久的。
redo log buffer
–innodb_log_buffer_size:通常8M已经足够使用了(上图是16M),重做日志缓冲一般不需要设置得很大,因为一般情况下每一秒钟会将重做日志缓冲刷新到日志文件,因此用户只需要保证每秒产生的事务量在这个缓冲大小之内即可。
由log block组成,每个log block 512字节(不需要double write)
为什么不需要double write呢?
由于重做日志块的大小和磁盘扇区大小一样,都是512字节,因此重做日志的写入可以保证原子性,不需要double write
redo log file
–innodb_log_file_size:线上应该设置得大一些 表示物理上的位于innodb存储引擎数据目录下的ib_logfile0,ib_logfile1大小?
–innodb_log_files_in_group:可以看到file0,file1两个文件
重做日志实现Durable的过程:
InnoDB是事务的存储引擎,其通过Force Log at Commit机制实现事务的持久性,即当事务提交(COMMIT)时,必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的COMMIT操作完成才算完成。
-
问题1:ib_logfile0和ib_logfile1是循环覆盖的,并不做归档。为什么不需要归档呢?
原因是mysql中有二进制日志
-
问题2:为什么要把重做日志设得很大呢?
因为redo log是做覆盖的,一个一个512字节的block来写,如果我将覆盖写一个block,但是这个block中对应的脏页还没有刷盘,那么写redo log就需要进行等待了(强制进行一次脏页的刷盘),而导致mysql数据库性能有很大程度的下降。
redo log buffer刷新条件
-
master thread每秒进行刷新
-
redo log buffer使用大于1/2进行刷新
-
事务提交时进行刷新
innodb_flush_log_at_timeout 参数,可以设置刷新间隔,默认为1
innodb_flush_log_at_trx_commit={0|1|2}
0 - 事务提交的时候并不把日志(redo log buffer)写入到磁盘(1s或者大于1/2时刷日志)
1 - 事务每次提交的时候要确保日志(redo log buffer)写入磁盘,即使宕机,也可以通过redo恢复,达到持久性的要求
2 - 事务提交的时候,仅将日志(redo log buffer)写入到操作系统缓存
0的话可能会有事务数据丢失。最坏的情况是丢失1秒的事务数据
1的事务数据是不会丢失的,为什么呢?
详细分析一下这个过程
上图中存在两个问题
fsync到本地磁盘文件ib_logfile0或ib_logfile1中
通过double write刷盘到磁盘中表的.ibd文件中
2的话,如果是mysql实例挂了,那么在mysql重启后,由于数据仍在缓存中,还是会继续写入redo日志的,不会丢失事务数据;但如果是服务器down了,那么这部分事务日志还是丢失了
redo log里面记录的到底是什么呢?
redo日志的分类
物理日志:记录整个页的变化(diff)
逻辑日志:Like SQL语句
物理逻辑日志:根据页进行记录,记录的是内容逻辑
space + page_no 指定页
redo log body:记录逻辑日志,并不是记录sql语句,记录的是页的变化
格式如下面两个insert/delete格式
redo log与binlog对比
在事务提交的时候,并不仅仅写redo log,还会记录binlog。为什么需要记录两份呢?
原因与mysql的基础架构有关。mysql支持多种存储引擎,那么redo log只是innodb引擎使用的,那么如何保证跨引擎的情况呢?在mysql的上层还需要记录到binlog中
二进制日志(binlog),其用来进行point in time(PIT 基于时间点的恢复)的恢复及主从复制(Replication)环境的建立。
从表面上看其和重做日志非常相似,都是记录了对于数据库操作的日志。然而,从本质上来看,两者有着非常大的不同。
-
重做日志是在InnoDB存储引擎层产生,而二进制日志是在MySQL数据库的上层产生的,并且二进制日志不仅仅针对于InnoDB存储引擎,MySQL数据库中的任何存储引擎对于数据库的更改都会产生二进制日志。
-
两种日志记录的内容形式不同。MySQL数据库上层的二进制日志是一种逻辑日志,其记录的是对应的SQL语句。而InnoDB存储引擎层面的重做日志是物理格式日志,其记录的是对于每个页的修改。
-
两种日志记录写入磁盘的时间点不同,二进制日志只在事务提交完成后进行一次写入。而InnoDB存储引擎的重做日志在事务进行中不断地被写入,这表现为日志并不是随事务提交的顺序进行写入的。
*代表提交
二进制日志仅在事务提交时记录,并且对于每一个事务,仅包含对应事务的一个日志。而对于lnnoDB存储引擎的重做日志,由于其记录的是物理操作日志,因此每个事务对应多个日志条目,并且事务的重做日志写入是并发的,并非在事务提交时写入,故其在文件中记录的顺序并非是事务开始的顺序。
比如说我一个事务是更新一张几百万条记录的表,那么产生的binlog日志估计有几百M,那么我commit的时候就是commit这几百M的日志。这时候commit就会发生类似等待的效果(其在写binlog的日志)
假设redo log也是200M,但由于其每秒钟就会fsync一次,其在不停的写,所以并不会发生类似等待的效果
-
redo log是写在redo log buffer中的
而binlog是写在binlog cache中的,32K大小
比如说我一个大事务产生200M的日志,那么怎么办呢?装不下那就直接写到磁盘中去。
可以定期看一下binlog_cache_disk_use这个值是否比较大,如果比较大,那说明binlog cache可能不够大
mysql用在OLTP下,一般是并发量大+小事务的情况,那么binlog写入这个问题应该并没有什么关系
在mysql中有一个优化的点:就是把一个大事务拆成小事务去执行,原因1:这样执行起来比较快(binlog写的比较快)。原因2:主从复制的时候,延时会小
oracle中就没有binlog,只用redo log,复制是基于redo log来做
保证redo log与bin log日志一致性
我们事务提交的时候,要写redo log和binlog,
-
为什么要保证两者的一致性呢?
其实是为了保证主从复制的一致性
-
如何保证这两个日志的一致性呢?
在这里面使用了mysql内部分布式事务,类似TCC
当事务commit之后,会执行以下操作
-
innodb redo log 的 prepare log
-
write binary log file
-
innodb redo log 的 commit log
这里的写指的都是写入到磁盘上
第一步先写innodb redo log的prepare的日志,第二步写mysql的binlog的日志,第三步写innodb redo log的commit日志
prepare只是写一个xid
-
情况1:如果只有第一步成功了,第二步失败了,
那么整个事务就会rollback
-
情况2:当事务提交的时候,如果我prepare日志写入成功了,二进制日志写入成功了,但是第三步在写commit日志的时候,服务器down机了。
那么我起来恢复的时候,我可以检测到prepare log中记录了一个xid,binlog中也有这个xid,这两个log中都有这个xid,就说明这个事务一定要提交,不管第三步有没有这个commit log了。也就是说这个事务全部都执行完了,就差最后的commit语句了,那么再恢复的时候,只要执行commit就可以了。
没开binlog的话,那么1,2都不写,只写3,恢复的时候,如果redo log没有commit log,那么就回滚
组提交 group commit
通过下面的复制图可以看到 第一步 和 第二步 都是group commit的
组提交中某个失败,那么只是rollback这一个事务
大家一定要注意:
无论是写redo log,还是写binlog,都是先写缓存,再fsync到磁盘
innodb_flush_method=O_DIRECT 打开O_DIRECT只保证数据写入到磁盘,元数据并没有更新,元数据指定是描述文件的信息,如文件的大小,文件的更新时间。所以无论用不用O_DIRECT,都要使用fsync
一次fsync刷新多个事务
性能提高10~100+倍
先fwrite到OS Cache
再fsync,确保数据刷新到磁盘
对于重做日志,fsync的作用是什么?
确保内存中的log buffer中的重做日志刷新到磁盘文件中ib_logdata0, ib_logdate1
磁盘IOPS决定TPS,TPS决定fsync的数量
那如何在IOPS比较少的情况下增加TPS呢?
数据库每秒钟只允许100条数据的update,这其实是很慢的。
如果我的fsync每次只更新一个事务,那就很慢了。
如果每次fsync 事务group,那么其TPS就增加了
组提交默认是开启的
使用checkpoint检查点技术解决redo log不可用问题
什么情况下重做日志不可用呢?
重做日志出现不可用的情况是因为当前事务数据库系统对重做日志的设计都是循环使用的,并不是让其无限增大的,这从成本及管理上都是比较困难的。重做日志可以被重用的部分是指这些重做日志已经不再需要,即当数据库发生宕机时,数据库恢复操作不需要这部分的重做日志,因此这部分就可以被覆盖重用。若此时重做日志还需要使用,那么必须强制产生Checkpoint,将缓冲池中的页至少刷新到当前重做日志的位置。
脏页是如何产生的
- 数据页首先被读入缓冲池中,当数据页中的某几条记录被更新或者插入新的记录时,所有的操作都是在Buffer Pool先完成的;
- Buffer Pool中的某个页和磁盘中的某个页在(Space, Page_Number)上是相同的,但是其内容可能是不同的(Buffer Pool中的被更新过了),形成了脏页;
- 要定期将缓冲池中的脏页刷回磁盘(Flush),达到最终一致,即通过CheckPoint机制来刷脏页;
问题:在buffer pool已经修改了,还没有刷回去到磁盘之前,数据库发生down机了,怎么办呢?
down机了,也没关系,因为每个页在修改的时候,都将日志写入了redo log。
那么mysql重启了,通过回放redo log,还是可以实现磁盘数据更新了的
如果按照这个思路,那我完全不需要checkpoint啊,你永远不刷新脏页都可以,因为我有redo log,但问题就是如果redo log很大很大,那恢复起来也非常耗时,所以其实checkpoint是一种
缩短数据库恢复时间
的技术
checkpoint检查点技术就是当缓冲池不够用时,将脏页刷新到磁盘,checkpoint技术的基础是LSN
checkpoint 所做的事情无外乎是将缓冲池中的脏页刷回到磁盘。不同之处在于刷新多少页到磁盘,每次从哪里取脏页,以及什么时间触发Checkpoint
redo log 基于LSN进行恢复
LSN(log sequence number)是什么呢?
比如一个新的页,LSN是有一个初始值的,比如说16,这时我对这个页进行了修改,那会产生redo log日志,比如说这个日志为10个字节,LSN就变为26,这个时候又进行了修改,产生redo log 20个字节,LSN就变为46了,所以LSN是一个单调递增的值,这个是重做日志中的LSN
在每个页中也有LSN,表示我这个页在做修改的时候,在做checkpoint的时候,我的LSN值是多少(即当时对应的重做日志的LSN是多少)
LSN是8字节的数字,其单位是字节。每个页有LSN,重做日志中也有LSN,Checkpoint 也有LSN
页中的LSN用来判断页是否需要进行恢复操作
例如,页P1的LSN为10000,而数据库启动时,lnnoDB检测到写入重做日志中的LSN为13000,并且该事务已经提交,那么数据库需要进行恢复操作,将重做日志应用到P1页中。
InnoDB存储引擎在启动时不管上次数据库运行时是否正常关闭,都会尝试进行恢复操作。因为重做日志记录的是物理日志,因此恢复的速度比逻辑日志,如二进制日志要快很多。
看一下存在的几个LSN
- log sequence number:表示当前在内存中重做日志的LSN的值
- log flushed up to:表示写到磁盘上重做日志的LSN的值
- pages flushed up to:页刷新的时候,最新的LSN的值,表示checkpoint已经刷新到磁盘页上的LSN
- last checkpoint:页刷新的时候,最oldest的LSN的值。checkpoint是根据页的oldest LSN对脏页列表进行排序的,但是写入的页是newest的LSN,所以除非这个页只改了一次,只有这时oldest=newest,之后都是3>4,其值越大说明需要提升checkpoint的跟进速度
1和2的值可以是不同的,因为我重做日志也是先写在内存里面,然后再刷新到文件里面,上面两个值,一个是内存,一个是磁盘,所以也可能是不一样的
2=3,说明所有页都已经刷回磁盘
2-3=脏页还未刷新到磁盘的大小
fuzzy checkout技术,是将部分脏页刷新回磁盘,对系统影响较小。
到底是多少脏页呢?是由这个值来决定的,所以这个参数很重要,值大一点,那么写的能力
就会提高起来,但如果值很大,那么可能就会hang住
这个参数是innodb调优需要很关注,但是很容易忽略的一个点
double write技术保证redo log有效
目的:提高数据写入的可靠性
当发生数据库宕机时,可能lnnoDB存储引擎正在写入某个页到表中
,而这个页只写了一部分,比如16KB 的页,只写了前4KB,之后就发生了宕机,这种情况被称为部分写失效(partial page write)。在InnoDB 存储引擎未使用double write技术前,曾经出现过因为部分写失效而导致数据丢失的情况。
通过redo log进行恢复,那么磁盘上的这个页一定要是完整干净的。发生partial page write,磁盘上的这个页已经corrupt了(非物理介质的corrupt)。比如说:我向redo日志中写入一条记录,对这个页进行reorganize,但这个页已经corrupt了,其无法进行reorganize的。
也就是说,在向表中写一个脏页之前,一定要有一个地方去记录这个页的副本
这就是说,在对磁盘上的某个页应用(apply)重做日志前,用户需要一个页的副本,当写入失效发生时,先通过页的副本在磁盘上还原该页,再进行重做,这就是double write。
具体做法:
double write 由两部分组成,一部分是内存中的doublewrite buffer,大小为2MB,另一部分是物理磁盘上共享表空间(double write段)中连续的128个页128*16=2M,即2个区(extent),大小同样为2MB。
在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是会通过memcpy函数将脏页先复制到内存中的doublewrite buffer,之后通过doublewrite buffer再分两次,每次1MB顺序地写入共享表空间的物理磁盘上,然后马上调用fsync 函数,同步磁盘,避免缓冲写带来的问题。
在完成doublewrite页的写入后,再将doublewrite buffer 中的页写入各个表空间文件.ibd文件
中,此时的写入则是离散的,通过space和page_no刷新到.ibd文件中
如果写入到共享表空间的时候发生partial page write呢?
因为我磁盘上.ibd文件中的页还是干净的,没有发生corrupt,数据还是一致的吧,这些页仍然可以通过redo进行恢复,因为这个页仍处于一致的状态,并没有不一致。
如果我写到.ibd文件中发生partial write,那么在double write这个对象里面,是不是有这个页的一个副本,我把对应这个页的副本(最新的)copy到.ibd文件中,然后再通过redo进行恢复。
即任意一个时刻,总有一个干净的页的存在