任何项目都会有日志,MySQL
也不例外,而且MySQL
更是其中的佼佼者,日志种类繁多,而本篇的目的就是全解MySQL
中的各类日志,如撤销日志、错误日志、慢查询日志、中继日志、回滚日志.....
其实日志的作用不言而喻,无论是线上排查,亦或是性能优化,几乎都需要从日志中来获得信息作为依据,而MySQL
中,很多很多的功能也都需要基于日志实现,比如事务回滚、数据持久化、数据恢复、数据迁移、MVCC
机制.....,因此本篇去阐述日志,也是为了方便撰写后续的其他文章。
OK,废话不多说了,咱们现在就开始吧~
一、Undo-log撤销日志
Undo
即撤销的意思,但咱们通常也习惯称它为回滚日志,在日常开发过程中,如果代码敲错了,一般会习惯性的按下Ctrl+Z
撤销,而Undo-log
的作用也是如此,但它是用来给MySQL
撤销SQL
操作的。
在之前的《SQL执行篇》中曾聊到过,当一条写入类型的SQL
执行时,都会记录Undo-log
日志,会生成相应的反SQL
放入到Undo-log
中,例如:
- 如果目前是
insert
插入操作,则生成一个对应的delete
操作。 - 如果目前是
delete
删除操作,InnoDB
中会修改隐藏字段deleted_bit=1
,则生成改为0
的语句。 - 如果目前的
update
修改操作,比如将姓名从竹子改成了熊猫,那就生成一个从熊猫改回竹子的操作。
当事务中某条SQL
执行失败时,MySQL
就需要回滚事务中其他执行成功的SQL
,此时就会找到这个事务在Undo-log
中生成的反SQL
,然后将库中的数据改回事务发生前的样子。
大家看这段话,似乎没有啥问题对不?也包括我之前的文章中也是这样去描述的,但这里其实存在些许误导,在之前的《SQL执行篇》中,我们说一条写
SQL
执行前,会生成对应的反SQL
记录在Undo-log
中,但实际上并不会生成反SQL
,这样去叙述仅是为了方便大家理解罢了。
那咱们怎么证明不会生成反SQL
呢?如果大家有仔细研究过MySQL
的日志,应该会发现Undo-log
并不存在单独的日志文件,也就是磁盘中并不会存在xx-undo.log
这类的文件,那Undo-log
存在哪儿呢?InnoDB
默认是将Undo-log
存储在xx.ibdata
共享表数据文件当中,默认采用段的形式存储。
也就是当一个事务尝试写某行表数据时,首先会将旧数据拷贝到xx.ibdata
文件中,将表中行数据的隐藏字段:roll_ptr
回滚指针会指向xx.ibdata
文件中的旧数据,然后再写表上的数据。
那
Undo-log
究竟在xx.ibdata
文件中怎么存储呢?在共享表数据文件中,有一块区域名为Rollback Segment
回滚段,每个回滚段中有1024
个Undo-log Segment
,每个Undo
段可存储一条旧数据,而执行写SQL
时,Undo-log
就是写入到这些段中。
不过在MySQL5.5
版本前,默认只有一个Rollback Segment
,而在MySQL5.5
版本后,默认有128
个回滚段,即支持128*1024
条Undo
记录同时存在。
1.1、对于事务回滚原理的纠正
结合上述纠正后的内容,咱们再对事务的回滚原理稍作更正,实际上当一个事务需要回滚时,本质上并不会以执行反SQL
的模式还原数据,而是直接将roll_ptr
回滚指针指向的Undo
记录,从xx.ibdata
共享表数据文件中拷贝到xx.ibd
表数据文件,覆盖掉原本改动过的数据。
还是上个图简单理解一下吧,如下:
一条写SQL
执行的流程如上图中的序号所示,当需要回滚事务时,直接用Undo
旧记录覆盖表中修改过的新记录即可!
如果是
insert
操作,由于插入之前这条数据都不存在,那么就不会产生Undo
记录,此时回滚时如何删除这条记录呢?因为插入操作不会产生Undo
旧记录,因此隐藏字段中的roll_ptr=null
,因此直接用null
覆盖插入的新记录即可,这样也就实现了删除数据的效果~
1.2、基于Undo版本链实现MVCC
认真阅读过《MySQL-MVCC机制原理剖析》的小伙伴对于这点并不陌生,Undo-log
中记录的旧数据并不仅仅只有一条,一条相同的行数据可能存在多条不同版本的Undo
记录,内部会通过roll_ptr
回滚指针,组成一个单向链表,而这个链表则被称之为Undo
版本链,案例如下:
-- 事务T1:trx_id=1(两次修改同一条数据)
UPDATE `zz_users` SET user_name = "竹子" WHERE user_id = 1;
UPDATE `zz_users` SET user_sex = "男" WHERE user_id = 1;
复制代码
Undo-log
中的旧数据版本链示意图大致如下:
当然,对于如何利用版本链实现MVCC
机制,这点就不反复赘述了,没了解过的可以去看关于MVCC
原理剖析的那篇文章。
1.3、Undo-log的内存缓冲区
InnoDB
在MySQL
启动时,会在内存中构建一个BufferPool
,而这个缓冲池主要存放两类东西,一类是数据相关的缓冲,如索引、锁、表数据等,另一类则是各种日志的缓冲,如Undo、Bin、Redo....
等日志。
而当一条写SQL
执行时,不会直接去往磁盘中的xx.ibdata
文件写数据,而是会写在undo_log_buffer
缓冲区中,因为工作线程直接去写磁盘太影响效率了,写进缓冲区后会由后台线程去刷写磁盘。
OK~,那么如果当一个事务提交时,
Undo
的旧记录会不会立马被删除呢?因为事务都提交了,不需要再回滚改动过的数据,似乎用不上Undo
旧记录了,对吗?确实如此,但不会立马删除Undo
记录,对于旧记录的删除工作,InnoDB
中会有专门的purger
线程负责,purger
线程内部会维护一个ReadView
,它会以此作为判断依据,来决定何时移除Undo
记录。
为什么不是事务提交后立马删除Undo
记录呢?因为可能会有其他事务在通过快照,读Undo
版本链中的旧数据,直接移除可能会导致其他事务读不到数据,因此删除的工作就交给了purger
线程。
1.4、Undo-log相关的参数
最后再来看看关于Undo-log
的一些参数,其实在MySQL5.5
之前没有太多参数,如下:
innodb_max_undo_log_size
:本地磁盘文件中,Undo-log
的最大值,默认1GB
。innodb_rollback_segments
:指定回滚段的数量,默认为1
个。
除开上述两个参数外,其他参数基本上是在MySQL5.6
才有的,如下:
innodb_undo_directory
:指定Undo-log
的存放目录,默认放在.ibdata
文件中。innodb_undo_logs
:指定回滚段的数量,默认为128
个,也就是之前的innodb_rollback_segments
。innodb_undo_tablespaces
:指定Undo-log
分成几