来源:https://blog.csdn.net/demonson/article/details/104369733
innodb事务日志包括redo log和undo log。redo log是重做日志,提供前滚操作,undo log是回滚日志,提供回滚操作。
undo log不是redo log的逆向过程,其实它们都算是用来恢复的日志:
1.redo log通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。
2.undo用来回滚行记录到某个版本。undo log一般是逻辑日志,根据每行记录进行记录。
1.redo log
1.1 redo log和二进制日志的区别
也能实现重做的功能,但是它们之间有很大区别。
-
二进制日志是在
存储引擎的上层产生的,不管是什么存储引擎,对数据库进行了修改都会产生二进制日志。而redo log是innodb层产生的,只记录该存储引擎中表的修改。
并且二进制日志先于redo log被记录。具体的见后文group commit小结。
-
二进制日志记录操作的方法是逻辑性的语句。即便它是基于行格式的记录方式,其本质也还是逻辑的SQL设置,如该行记录的每列的值是多少。而redo log是在物理格式上的日志,它记录的是数据库中每个页的修改。
-
二进制日志只在每次事务提交的时候一次性写入缓存中的日志”文件”(对于非事务表的操作,则是每次执行语句成功后就直接写入)。而redo log在数据准备修改前写入缓存中的redo log中,然后才对缓存中的数据执行修改操作;而且保证在发出事务提交指令时,先向缓存中的redo log写入日志,写入完成后才执行提交动作。
-
因为二进制日志只在提交的时候一次性写入,所以二进制日志中的记录方式和提交顺序有关,且一次提交对应一次记录。而redo log中是记录的物理页的修改,redo log文件中同一个事务可能多次记录,最后一个提交的事务记录会覆盖所有未提交的事务记录。例如事务T1,可能在redo log中记录了 T1-1,T1-2,T1-3,T1* 共4个操作,其中 T1* 表示最后提交时的日志记录,所以对应的数据页最终状态是 T1* 对应的操作结果。而且redo log是并发写入的,不同事务之间的不同版本的记录会穿插写入到redo log文件中,例如可能redo log的记录方式如下:T1-1,T1-2,T2-1,T2-2,T2*,T1-3,T1* 。
-
事务日志记录的是物理页的情况,它具有幂等性,因此记录日志的方式极其简练。幂等性的意思是多次操作前后状态是一样的,例如新插入一行后又删除该行,前后状态没有变化。而二进制日志记录的是所有影响数据的操作,记录的内容较多。例如插入一行记录一次,删除该行又记录一次。
1.2 redo log的基本概念
force log at commit机制实现事务的持久性,即在事务提交的时候,必须先将该事务的所有事务日志写入到磁盘上的redo log file和undo log file中进行持久化。
在此处需要注意一点,一般所说的log file并不是磁盘上的物理日志文件,而是操作系统缓存中的log file,官方手册上的意思也是如此(例如:With a value of 2, the contents of the
InnoDB log buffer are written to the log file after each transaction commit and
the log file is flushed to disk approximately once per second)。
但说实话,这不太好理解,既然都称为file了,应该已经属于物理文件了。所以在本文后续内容中都以os buffer或者file system buffer来表示官方手册中所说的Log file,然后log file则表示磁盘上的物理日志文件,即log file on disk。
另外,之所以要经过一层os buffer,是因为open日志文件的时候,open没有使用O_DIRECT标志位,该标志位意味着绕过操作系统层的os buffer,IO直写到底层存储设备。不使用该标志位意味着将日志进行缓冲,缓冲到了一定容量,或者显式fsync()才会将缓冲中的刷到存储设备。使用该标志位意味着每次都要发起系统调用。比如写abcde,不使用o_direct将只发起一次系统调用,使用o_object将发起5次系统调用。
-
当设置为1的时候,事务每次提交都会将log buffer中的日志写入os buffer并调用fsync()刷到log file on disk中。这种方式即使系统崩溃也不会丢失任何数据,但是因为每次提交都写入磁盘,IO的性能较差。
-
当设置为0的时候,事务提交时不会将log buffer中日志写入到os buffer,而是每秒写入os buffer并调用fsync()写入到log file on disk中。也就是说设置为0时是(大约)每秒刷新写入到磁盘中的,当系统崩溃,会丢失1秒钟的数据。
-
当设置为2的时候,每次提交都仅写入到os buffer,然后是每秒调用fsync()将os buffer中的日志写入到log file on disk。
-
如果启用了二进制日志,则设置sync_binlog=1,即每提交一次事务同步写到磁盘中。
-
总是设置innodb_flush_log_at_trx_commit=1,即每提交一次事务都写到磁盘中。
#创建测试表
drop table if exists test_flush_log;
create table test_flush_log(id int,name char(50))engine=innodb;
#创建插入指定行数的记录到测试表中的存储过程
drop procedure if exists proc;delimiter $$create procedure proc(i int)begin declare s int default 1; declare c char(50) default repeat('a',50); while s<=i do start transaction; insert into test_flush_log values(null,c); commit; set s=s+1; end while;end$$delimiter ;
当前环境下, innodb_flush_log_at_trx_commit 的值为1,即每次提交都刷日志到磁盘。测试此时插入10W条记录的时间。
mysql> call proc(100000);
Query OK, 0 rows affected (15.48 sec)
mysql> set @@global.innodb_flush_log_at_trx_commit=2;
mysql> truncate test_flush_log;
mysql> call proc(100000);
Query OK, 0 rows affected (3.41 sec)
mysql> set @@global.innodb_flush_log_at_trx_commit=0;
mysql> truncate test_flush_log;
mysql> call proc(100000);
Query OK, 0 rows affected (2.10 sec)
更好的插入数据的做法是将值设置为1,然后修改存储过程,将每次循环都提交修改为只提交一次,这样既能保证数据的一致性,也能提升性能,修改如下:
drop procedure if exists proc;
delimiter $$
create procedure proc(i int)
begin
declare s int default 1;
declare c char(50) default repeat('a',50);
start transaction;
while s<=i DO
insert into test_flush_log values(null,c);
set s=s+1;
end while;
commit;
end$$
delimiter ;
mysql> set @@global.innodb_flush_log_at_trx_commit=1;
mysql> truncate test_flush_log;
mysql> call proc(1000000);
Query OK, 0 rows affected (11.26 sec)
1.3 日志块(log block)
日志块头、日志块尾和日志主体。其中日志块头占用12字节,日志块尾占用8字节,所以每个redo log block的日志主体部分只有512-12-8=492字节。
-
log_block_hdr_no:(4字节)该日志块在redo log buffer中的位置ID。
-
log_block_hdr_data_len:(2字节)该log block中已记录的log大小。写满该log block时为0x200,表示512字节。
-
log_block_first_rec_group:(2字节)该log block中第一个log的开始偏移位置。
-
lock_block_checkpoint_no:(4字节)写入检查点信息的位置。
1.4 log group和redo log file
mysql> show global variables like "innodb_log%";
+-----------------------------+----------+
| Variable_name | Value |
+-----------------------------+----------+
| innodb_log_buffer_size | 8388608 |
| innodb_log_compressed_pages | ON |
| innodb_log_file_size | 50331648 |
| innodb_log_files_in_group | 2 |
| innodb_log_group_home_dir | ./ |
+-----------------------------+----------+
[root@xuexi data]# ll /mydata/data/ib*
-rw-rw---- 1 mysql mysql 79691776 Mar 30 23:12 /mydata/data/ibdata1
-rw-rw---- 1 mysql mysql 50331648 Mar 30 23:12 /mydata/data/ib_logfile0
-rw-rw---- 1 mysql mysql 50331648 Mar 30 23:12 /mydata/data/ib_logfile1
1.5 redo log的格式
-
redo_log_type:占用1个字节,表示redo log的日志类型。
-
space:表示表空间的ID,采用压缩的方式后,占用的空间可能小于4字节。
-
page_no:表示页的偏移量,同样是压缩过的。
-
redo_log_body表示每个重做日志的数据部分,恢复时会调用相应的函数进行解析。例如insert语句和delete语句写入redo log的内容是不一样的。
1.6 日志刷盘的规则
1.7 数据页刷盘的规则及checkpoint
不管怎样,checkpoint触发后,会将buffer中脏数据页和脏日志页都刷到磁盘。
-
sharp checkpoint:在重用redo log文件(例如切换日志文件)的时候,将所有已记录到redo log中对应的脏数据刷到磁盘。
-
fuzzy checkpoint:一次只刷一小部分的日志到磁盘,而非将所有脏日志刷盘。有以下几种情况会触发该检查点:
-
master thread checkpoint:由master线程控制,
每秒或每10秒刷入一定比例的脏页到磁盘。
-
flush_lru_list checkpoint:从MySQL5.6开始可通过 innodb_page_cleaners 变量指定专门负责脏页刷盘的page cleaner线程的个数,该线程的目的是为了保证lru列表有可用的空闲页。
-
async/sync flush checkpoint:同步刷盘还是异步刷盘。例如还有非常多的脏页没刷到磁盘(非常多是多少,有比例控制),这时候会选择同步刷到磁盘,但这很少出现;如果脏页不是很多,可以选择异步刷到磁盘,如果脏页很少,可以暂时不刷脏页到磁盘
-
dirty page too much checkpoint:脏页太多时强制触发检查点,目的是为了保证缓存有足够的空闲空间。too much的比例由变量 innodb_max_dirty_pages_pct 控制,MySQL 5.6默认的值为75,即当脏页占缓冲池的百分之75后,就强制刷一部分脏页到磁盘。
MySQL停止时是否将脏数据和脏日志刷入磁盘,由变量innodb_fast_shutdown={ 0|1|2 }控制,默认值为1,即停止时只做一部分purge,忽略大多数flush操作(但至少会刷日志),在下次启动的时候再flush剩余的内容,实现fast shutdown。
1.8 LSN超详细分析
fil_page_lsn记录了当前页最终的LSN值是多少。通过数据页中的LSN值和redo log中的LSN值比较,如果页中的LSN值小于redo log中LSN值,则表示数据丢失了一部分,这时候可以通过redo log的记录来恢复到redo log中记录的LSN值时的状态。
mysql> show engine innodb stauts
---
LOG
---
Log sequence number 2225502463
Log flushed up to 2225502463
Pages flushed up to 2225502463
Last checkpoint at 2225502463
0 pending log writes, 0 pending chkp writes
3201299 log i/o's done, 0.00 log i/o's/second
-
log sequence number就是当前的redo log(in buffer)中的lsn;
-
log flushed up to是刷到redo log file on disk中的lsn;
-
pages flushed up to是已经刷到磁盘数据页上的LSN;
-
last checkpoint at是上一次检查点所在位置的LSN。
log sequence number(110) > log flushed up to(100) = pages flushed up to = last checkpoint at
log sequence number(150) = log flushed up to > pages flushed up to(100) = last checkpoint at
log sequence number > log flushed up to 和 pages flushed up to > last checkpoint at
log sequence number = log flushed up to > pages flushed up to = last checkpoint at
1.9 innodb的恢复行为
1.10 和redo log有关的几个变量
-
innodb_flush_log_at_trx_commit={0|1|2} # 指定何时将事务日志刷到磁盘,默认为1。
-
0表示每秒将”log buffer”同步到”os buffer”且从”os buffer”刷到磁盘日志文件中。
-
1表示每事务提交都将”log buffer”同步到”os buffer”且从”os buffer”刷到磁盘日志文件中。
-
2表示每事务提交都将”log buffer”同步到”os buffer”但每秒才从”os buffer”刷到磁盘日志文件中。
-
innodb_log_buffer_size:# log buffer的大小,默认8M
-
innodb_log_file_size:#事务日志的大小,默认5M
-
innodb_log_files_group =2:# 事务日志组中的事务日志文件个数,默认2个
-
innodb_log_group_home_dir =./:# 事务日志组路径,当前目录表示数据目录
-
innodb_mirrored_log_groups =1:# 指定事务日志组的镜像组个数,但镜像功能好像是强制关闭的,所以只有一个log group。在MySQL5.7中该变量已经移除。
2.undo log
2.1 基本概念
可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。
undo log也会产生redo log,因为undo log也要实现持久性保护。
2.2 undo log的存储方式
rollback segment称为回滚段,每个回滚段中有1024个undo log segment。
[root@xuexi data]# ll /mydata/data/ib*
-rw-rw---- 1 mysql mysql 79691776 Mar 31 01:42 /mydata/data/ibdata1
-rw-rw---- 1 mysql mysql 50331648 Mar 31 01:42 /mydata/data/ib_logfile0
-rw-rw---- 1 mysql mysql 50331648 Mar 31 01:42 /mydata/data/ib_logfile1
2017-03-31 13:16:00 7f665bfab720 InnoDB: Expected to open 3 undo tablespaces but was able
2017-03-31 13:16:00 7f665bfab720 InnoDB: to find only 0 undo tablespaces.
2017-03-31 13:16:00 7f665bfab720 InnoDB: Set the innodb_undo_tablespaces parameter to the
2017-03-31 13:16:00 7f665bfab720 InnoDB: correct value and retry. Suggested value is 0
2.3 和undo log相关的变量
mysql> show variables like "%undo%";
+-------------------------+-------+
| Variable_name | Value |
+-------------------------+-------+
| innodb_undo_directory | . |
| innodb_undo_logs | 128 |
| innodb_undo_tablespaces | 0 |
+-------------------------+-------+
2.4 delete/update操作的内部机制
-
delete操作实际上不会直接删除,而是将delete对象打上delete flag,标记为删除,最终的删除操作是purge线程完成的。
-
update分为两种情况:update的列是否是主键列。
-
如果不是主键列,在undo log中直接反向记录是如何update的。即update是直接进行的。
-
如果是主键列,update分两部执行:先删除该行,再插入一行目标行。
3.binlog和事务日志的先后顺序及group commit
如果事务不是只读事务,即涉及到了数据的修改,默认情况下会在commit的时候调用fsync()将日志刷到磁盘,保证事务的持久性。
但是一次刷一个事务的日志性能较低,特别是事务集中在某一时刻时事务量非常大的时候。innodb提供了group commit功能,可以将多个事务的事务日志通过一次fsync()刷到磁盘中。
因为事务在提交的时候不仅会记录事务日志,还会记录二进制日志,但是它们谁先记录呢?二进制日志是MySQL的上层日志,先于存储引擎的事务日志被写入。
在MySQL5.6以前,当事务提交(即发出commit指令)后,MySQL接收到该信号进入commit prepare阶段;进入prepare阶段后,立即写内存中的二进制日志,写完内存中的二进制日志后就相当于确定了commit操作;然后开始写内存中的事务日志;最后将二进制日志和事务日志刷盘,它们如何刷盘,分别由变量 sync_binlog 和 innodb_flush_log_at_trx_commit 控制。
但因为要保证二进制日志和事务日志的一致性,在提交后的prepare阶段会启用一个锁来保证它们的顺序性和一致性。但这样会导致开启二进制日志后group commmit失效,特别是在主从复制结构中,几乎都会开启二进制日志。
在MySQL5.6中进行了改进。提交事务时,在存储引擎层的上一层结构中会将事务按序放入一个队列,队列中的第一个事务称为leader,其他事务称为follower,leader控制着follower的行为。虽然顺序还是一样先刷二进制,再刷事务日志,但是机制完全改变了:删除了原来的prepare_commit_mutex行为,也能保证即使开启了二进制日志,group commit也是有效的。
MySQL5.6中分为3个步骤:
-
flush阶段:向内存中写入每个事务的二进制日志。
-
sync阶段:将内存中的二进制日志刷盘。若队列中有多个事务,那么仅一次fsync操作就完成了二进制日志的刷盘操作。这在MySQL5.6中称为BLGC(binary log group commit)。
-
commit阶段:leader根据顺序调用存储引擎层事务的提交,由于innodb本就支持group commit,所以解决了因为锁 prepare_commit_mutex 而导致的group commit失效问题。
长按订阅更多精彩▼
免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!