MySQL三大日志(binlog,redo log,undo log)详解
热衷学习,热衷生活!😄
沉淀、分享、成长,让自己和他人都能有所收获!😄
一、MySQL日志
MySQL
日志主要包括错误日志、查询日志、慢查询日志、事务日志、二进制日志几大类。其中比较重要的就是二进制日志binlog
(归档日志)、事务日志redo log
(重做日志)和undo log
(回滚日志)。
日志关系如下图:
二、redo log
redo log
(重做日志)是InnoDB
存储引擎独有的,它让MySQL
有了崩溃恢复的能力。
当MySQL
实例挂了或者宕机了,重启的时候InnoDB
存储引擎会使用rede log
日志恢复数据,保证事务的持久性和完整性。如下图:
MySQL
中数据是以页为单数存储,当你查询一条记录时,硬盘会把一整页的数据加载出来,加载出来的数据叫做数据页,会放到Buffer Pool
中。后续的查询都是先从Buffer Pool
中找,没有找到再去硬盘加载其他的数据页直到命中,这样子可以减少磁盘IO
的次数,提高性能。更新数据的时候也是一样,优先去Buffer Pool
中找
,如果存在需要更新的数据就直接更新。然后会把“在某个数据页做了什么修改”记录到重做日志缓存(redo log buffer
)里,在刷盘的时候会写入redo log
日志文件里。
如下图:
小贴士:每条redo记录由“表空间号+数据页号+偏移量+修改数据长度+具体修改的数据”组成。
刷盘时机
理想情况下,事务一提交就会进行刷盘操作,但是实际上是刷盘的时机是根据策略来决定的。
InnoDB
存储引擎为redo log
的刷盘策略提供了innodb_flush_log_at_trx_commit
参数,它支持三种策略:
- 0:设置为0的时候,每次提交事务时不刷盘。
- 1:设置为1的时候,每次提交事务时刷盘。
- 2:设置为2的时候,每次提交事务时都只把
redo log buffer
写入page cache
。
innodb_flush_log_at_trx_commit
参数默认为1,当事务提交的时候会调用fsync
对redo log
进行刷盘,将redo log buffer
写入redo log
文件中。
另外,Innodb
存储引擎有一个后台线程,每隔1
秒,就会把会redo log buffer
中的内容写入到文件系统缓存page cache
,然后调用fsync
刷盘。
如上图,所以说一个没有提交事务的redo log
记录,也会被刷盘。
下面是各种刷盘策略的流程图。
innodb_flush_log_at_trx_commit = 0
如上图,如果宕机了或者MySQL
挂了可能造成1
秒内的数据丢失。
innodb_flush_log_at_trx_commit = 1
如上图,只要事务提交成功,redo log
记录就一定在磁盘里,不会有任务数据丢失。
如果执行事务的时候MySQL
挂了或者宕机了,这部分日志丢失了,但是因为事务没有提交,所以日志丢了也不会有损失。
innodb_flush_log_at_trx_commit = 2
如上图,当事务提交成功时,redo log buffer
日志会被写入page cache
,然后后台线程会刷盘写入redo log
,由于后台线程是1
秒执行一次所以宕机或者MySQL
挂了可能造成1
秒内的数据丢失。
日志文件组
硬盘上存储的redo log
日志文件不止一个,而是一个日志文件组的形式出现的,每个的redo log
文件大小都是一样的。它采用的是环形数组形式,从头开始写,写到末尾回到头循环写,如下图所示:
在日志文件组中有两个重要的属性,分别是witre pos、checkpoint
- wirte pos:是当前记录的位置,一边写一边后移。
- checkpoint:是当前要擦除的位置,也是后台推移。
每次刷盘redo log
记录到日志文件组中,wirte log
位置就会后移更新。
每次MySQL
加载日志文件组恢复数据时,会清空加载过的redo log
,并把checkpoint
后移更新。
write pos
和 checkpoint
之间的还空着的部分可以用来写入新的 redo log
记录。
如果 witre pos
追上checkpoint
,表示日志文件组满了,这时候不能再写入新的redo log
记录,MySQL
得停下来,清空一些记录,把checkpoint
推荐一下。
redo log小结
redo log
的作用和它的刷盘时机、存储形式。
可以思考一个问题:只要每次把修改后的数据页直接刷盘不就好了,为什么还要用redo log
刷盘?不都是刷盘吗?有什么区别?
实际上,数据页大小是16KB
,刷盘比较耗时,可能就修改了数据页的几byte
数据,没有必要把整页的数据刷盘。而且数据页刷盘都是随机写,因为一个数据页对应的位置可能是在硬盘文件的随机位置,所以性能很差。
如果是写redo log
,一行记录就占了几十byte
,只要包含了表空间号、数据页号、磁盘文件偏移量、修改值,再加上是顺序写,所以刷盘效率很高。
所以用 redo log
形式记录修改内容,性能会远远超过刷数据页的方式,这也让数据库的并发能力更强。
三、binlog
redo log
是物理日志,记录的是“在某个数据页做了什么修改”,属于Innodb
存储引擎。
而binlog
日志是逻辑日志,记录内容是语句的原始逻辑,属于MySQL Server
层。所有的存储引擎只要发生了数据更新,都会产生binlog
日志。
binlog
日志的作用
可以说MySQL
数据库的数据备份、主备、主主、住从都离不开binlog
,需要依赖binlog
来同步数据,保证数据一致性。
binlog
会记录所有涉及更新数据的逻辑规则,并且按顺序写。
记录格式
binlog
日志有三种格式,可以通过binlog_format
参数设置,有以下三种:
- statement
- row
- mixed
设置statement
记录的内容是SQL
语句原文,比如执行一条update T set update_time = now() where id = 1
,记录内容如下:
同步数据时,会执行记录的SQL
语句,但是有个问题update_time = now()
这里会获取到当前系统问题,直接执行会导致与原库数据不一致。
为了解决这种问题,我们需要将binlog_format
设置成row
,记录的不再是简单的SQL
语句了,还包含了操作的具体数据,记录内容如下:
row
格式记录的内容看不到详细信息,通过mysqlbinlog
工具解析出来。
update_time = now()
变成了具体的时间,条件后面的@1、@2
都是该行数据第1个~2个字段的原始值(假设这张表只有2个字段)。
设置成row
带来的好处就是同步数据的一致性,通常情况都设置成row
,这样可以为数据库的恢复与同步带来更好的可靠性。但是这种格式需要大量的容量来记录,比较占用空间,恢复与同步时会更消耗IO
资源,影响执行速度。
所以又有了一种折中方案,设置为mixed
,记录的内容是前两者的混合。
MySQL
会判断这条SQL
语句是否会引起数据不一致,如果是就用row
格式,否则就用statement
格式。
写入机制
binlog
的写入时机为事务执行过程中,先把日志写到binlog cache
,事务提交的时候再把binlog cache
写到binlog
文件中(实际先会写入page cache
,然后再由fsync
写入binlog
文件)。
因为一个事务的binlog
不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一块内存作为binlog cache
。可以通过binlog_cache_size
参数控制单线程binlog_cache
大小,如果存储内容超过了这个参数,就要暂存到磁盘。
binlog
日志刷盘流程如下:
- 上图的
write
,是指把日志写入到文件系统的page cache
,并没有把数据持久化硬盘,所以速度比较快。 - 上图的
fsync
才是将数据库持久化到硬盘的操作。
write
和fsync
的时机可以由参数sync_binlog
控制,可以配置成0、1、N(N>1)
。
- 设置成0时:表示每次提交事务都只会
write
,由系统自行判断什么时候执行fsync
。 - 设置成1时:表示每次提交事务都会执行
fsync
,就和redo log
日志刷盘流程一样。 - 设置成N时:表示每次提交事务都会
write
,但是积累N
个事务后才fsync
。
设置为0时如下图:
从上图可知,sync_bilog = 0
设置成0
,只把日志写入page cache
虽然性能得到了提高,但是事务提交了fsync
的时候宕机了,可能造成binlog
日志的丢失。
设置为2时如下图:
在出现IO
瓶颈的场景里,将sync_binlog
设置成一个比较大的值,可以提升性能。
同样的,如果机器宕机,会丢失最近N
个事务的binlog
日志。
两阶段提交
redo log
(重做日志)让InnoDB
存储引擎有了崩溃恢复的能力。
binlog
(归档日志)保证了MySQL
集群架构数据的一致性。
虽然它们都属于持久化的保证,但是侧重点不一样。
在执行更新语句过程,会记录redo log
与binlog
两块日志,以基本的事务为单位,redo log
在事务执行过程中可以不断写入,而binlog
日志只有在提交事务的时候才会写入,所以它们写入的时机不一样。
思考一个问题,如果redo log和binlog两份日志之间的逻辑不一样,会出现什么问题呢?MySQL是怎么解决这个问题的呢?
比如有这样一个场景,假设有这么一条语句update T set c = 1 where id = 2
(c原值为0),假如执行过程中写完redo log
日志后,在写入binlog
的时候发生了异常,会出现什么情况呢?
如下图:
由于binlog
日志没写完就异常,这个时候binlog
日志里面没有对应的修改记录,之后使用binlog
同步的数据的时候就会少这一次的更新,这一行数据c = 0
,而原库使用redo log
日志恢复,这一行数据c = 1
,最终数据不一致。如下图:
为了解决两份日志之间的逻辑不一致的问题,InnoDB
存储引擎使用两阶段提交方案。
将redo log
日志的写入拆分成两个步骤prepare
和commit
,如下图:
使用两阶段提交后,写入binlog
时发生异常也没关系,因为MySQL
根据redo log
日志恢复数据时,发现redo log
日志处于prepare
阶段,并且没有对应binlog
日志(根据事务id对应),所以就会回滚事务。
再想一个场景,redo lgo
设置commit
阶段发生异常,事务会不会回滚呢?
并不会回滚事务,虽然redo log
是处于prepare
阶段,但是存在对应的事务binlog
日志,所以MySQL
认为是完整的,所以不会回滚事务。
undo log
想要保证事务的原子性,就需要在发生异常时,对已经执行的操作进行回滚,在MySQL
中恢复机制是通过undo log
(回滚日志)实现的,所有事务进行的修改都会先被记录到这个回滚日志,然后再执行其他相关的操作。如果执行过程中遇到异常的话,我们直接利用回滚日志中的信息将数据回滚到修改之前的样子。并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务。
另外,MVCC
的实现依赖:**隐藏字段、Read View
、undo log
**。在底层实现中,InnoDB
通过数据行的DB_TRX_ID
和Read View
来判断数据的可见性,如不可见,则通过数据行DB_ROLL_PTR
找到undo log
中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事物里,用户只能看到该事务创建Read View
之前已经提交的修改和该事务本身做的修改。
总结
MySQL InnoDB
引擎使用redo log
日志保证事务的持久性,使用undo log
日志保证事务的原子性。
MySQL
数据库的数据备份、主备、主主、主从离不开binlog
,需要依赖binlog
来同步数据,保证数据的一致性。