天天看点

【MySQL系列】- redo log知多少

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。​​点击跳转到网站。​​

InnoDB 存储引擎是以页为单位来管理存储空间的,我们的增删改查本质上都是对页面上进行操作。我们知道在访问磁盘的时候,MySQL是会把数据加载到Buffer Pool然后进行操作的。对于DML操作,表、索引等的增删改DDL操作,还有数据本身是在Buffer Pool缓冲池中可能还没来得及刷新到磁盘中,系统或者服务器突然崩溃,那这些数据该怎么恢复呢?

redo log是什么

官网定义:

A disk-based data structure used during crash recovery, to correct data written by incomplete transactions

翻译过来就是,redo log是一种基于磁盘的数据结构,用于在故障恢复期间纠正由不完整事务写入的数据。

redo log让MySQL innodb引擎有奔溃恢复的能力。redo log是保证事务的完整性、持久性,只有innodb引擎支持事务,所以redo log也是innodb引擎独有的。

redo log 格式

redo 日志本质上只是记录了一下事务对数据库做了哪些修改。 InnoDB 针对事务对数据库的不同修改场景定义了多种类型的redo log,但是绝大部分类型的 redo log都有下面这种通用的结构:

【MySQL系列】- redo log知多少
  • type:redo 日志的类型,redo 日志设计大约有53种不同的类型日志。
  • space ID:表空间 ID
  • page number:页号
  • data:该条 redo 日志的具体内容

mini-transaction

redo log是以组的形式写入磁盘的。插入或者修改一条数据都会对B+树进行修改,可能不止有一个修改点,甚至会涉及到页分裂。为了保证这组数据的原子性,MySQL引入了mini-transaction(简称mtr)的概念。

mtr是当在 DML 操作期间在物理级别对内部数据结构进行更改时,InnoDB 处理的一个内部阶段。mtr没有回滚的概念。一个所谓的mini-Transaction 可以包含一组 redo日志,在进行崩溃恢复时这一组redo 日志作为一个不可分割的整体。

一个事务可以包含若干条语句,每一条语句其实是由若干个 mini-Transaction组成,每一个 mini-Transaction 又可以包含若干条 redo日志,最终形成了一个树形结构。

LSN

LSN是log sequence number(日志序列号)的缩写,用于记录日志序号,它是一个不断递增的 unsigned long 类型的整数。LSN的初始值是8704,也就是说LSN从8704开始递增。每一组由Mini-Transaction 生成的 redo log都有一个唯一的LSN值与

其对应,LSN 值越小,说明 redo 日志产生的越早。通过LSN,可以具体的定位到其在redo log文件中的位置。

查看系统中的LSN值

可以通过SQL语句查看系统中LSN的值

SHOW ENGINE INNODB STATUS;      

在结果中找到如下片段

---
LOG
---
Log sequence number 1765718166
Log flushed up to   1765718166
Pages flushed up to 1765718166
Last checkpoint at  1765718157
0 pending log flushes, 0 pending chkp writes
212 log i/o's done, 0.00 log i/o's/second      
  • Log sequence number:系统中的 lsn 值,也就是当前系统已经写入的 redo日志量,包括写入log buffer中的日志。
  • Log flushed up to:flushed_to_disk_lsn 的值,也就是当前系统已经写入磁盘的 redo 日志量。
  • Pages flushed up to:代表 flush链表中被最早修改的那个页面对应的

    oldest_modification 属性值。

  • Last checkpoint at:当前系统的checkpoint_lsn值

Log buffer

为了解决磁盘速度过慢的问题而引入了Buffer Pool。同理,

写入 redo log时也不能直接直接写到磁盘上,所以引入了Log buffer。Log buffer(日志缓冲区)是保存要写入磁盘上日志文件的数据的内存区域,Log buffer可通过变量​​

​innodb_log_buffer_size​

​配置,默认是16M。Log buffer的数据会定期的刷新到磁盘中,增加日志缓冲区大小可以支持大型事务,这样无需在事务提交之前将redo log写入磁盘,节省磁盘I/O,

InnoDB 为了更好的进行系统崩溃恢复,把通过Mini-Transaction 生成的redo日志都放在了大小为 512 字节的块(block)中,向log buffer中写入 redo log的过程是顺序的,也就是先写入到block中,当该 block的空闲空间用完之后再往下一个 block中写。Mini-Transaction运行过程中产生的一组 redo log,在Mini-Transaction结束时这组redo log会被复制到log buffer 中。

redo log 刷盘时机

前面说到log buffer中的数据会定时刷新到磁盘,这就涉及到redo log 刷盘时机了。可能触发刷盘的情况如下:

  1. log buffer空间不足时:log buffer 的大小是有限的,如果不停的往这个有限大小的 log buffer里写入日志,很快它就会被填满。InnoDB认为如果当前写入 log buffer 的 redo log量已经占满了log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。
  2. 事务提交时:之所以使用redo log主要是因为它占用的空间少,还是顺序写,在事务提交时可以不把修改过的Buffer Pool 页面刷新到磁盘,但是为了保证持久性,必须要把修改这些页面对应的 redo log刷新到磁盘。

    变量​​

    ​innodb_flush_log_at_trx_commit​

    ​作用于事务提交时,innodb_flush_log_at_trx_commit 变量可配置3个值:
  • 当设置该值为1时,每次事务提交都要做一次刷盘,这是最安全的配置,即使宕机也不会丢失事务;也是默认值
  • 当设置为2时,则在事务提交时只做写操作,只保证写到系统的page cache,因此实例崩溃不会丢失事务,但宕机则可能丢失事务;
  • 当设置为0时,事务提交不会触发redo写操作,而是留给后台线程每秒一次的刷盘操作,因此实例崩溃将最多丢失1秒钟内的事务。
【MySQL系列】- redo log知多少

显然对性能的影响是随着持久化程度的增加而增加的。通常我们建议在日常场景将该值设置为1,但在系统高峰期临时修改成2以应对大负载。

  1. 后台线程:默认每秒都会刷新一次log buffer中的redo log到磁盘。可以通过变量​

    ​innodb_flush_log_at_timeout​

    ​来控制后台线程的刷新频率
  2. 正常关闭服务器时等等

崩溃恢复

在服务器不挂的情况下,redo 日志简直就是个大累赘,不仅没用,反而让性能变得更差。但是万一数据库挂了,就可以在重启时根据redo日志中的记录将页面恢复到系统崩溃前的状态。

MySQL可以根据redo log中的各种LSN值,来确定恢复的起点和终点。然后将 redo log中的数据,以哈希表的形式将一个页面下的放到哈希表的一个槽中。之后就可以遍历哈希表,因为对同一个页面进行修改的 redo log都放在了一个槽里,所以可以一次性将一个页面修复好(避免了很多读取页面的随机 IO)。并且通过各种机制,避免无谓的页面修复,比如已经刷新的页面,进而提升崩溃恢复的速度。

在MySQL 8.0.21版本中,可以通过​

​ALTER INSTANCE DISABLE INNODB REDO_LOG​

​语句来关闭redo Log,但是最好还是不要关闭。

redo log 参数

MySQL的数据目录下默认有两个名为ib_logfile0和ib_logfile1 的文件。可通过以下语句查看数据目录:

SHOW VARIABLES LIKE 'datadir'      

redo log的目录和大小都是可修改的。

  • innodb_log_group_home_dir:修改redo log生成目录,默认为当前数据目录所在文件夹下
  • innodb_log_file_size:单个redo log文件的大小,默认是48M
  • innodb_log_files_in_group:redo log文件组的文件个数,默认是2,最大100

redo log在磁盘是以redo log文件组的形式存在的,这些文件的文件名以ib_logfile开头,以数字[N]结尾,N从0开始,N为正整数。

redo log在写入文件时,先写入ib_logfile0,ib_logfile0写满了再写ib_logfile1,以此类推往下写,如果到最后一个文件也写满了,就从ib_logfile0重新开始写。

参考资料: