MySQL三大日志
一、概述
MySQL
日志主要包括错误日志、查询日志、慢查询日志、重做日志、归档日志几大类。其中比较重要的归档日志(binlog
)和重做日志(redo log
)和回滚日志(undo log
)。
二、undo log日志
2.1 概念
顾名思义,回滚日志(undo log
)的字面意思就是撤销操作的日志,指的是使MySQL
中的数据回到某个状态。
在MySQL
数据库中,事务开始之前,MySQL
会将待修改的记录保存到undo log
中,如果数据库崩溃或者事务需要回滚时,MySQL
可以通过利用undo log
日志,将数据库中的数据回滚到之前的状态。
MySQL
新增、修改和删除数据时,在事务开始前,就会将信息写入undo log
中。事务提交时,并不会立刻删除undo log
,InnoDB
存储引擎会将事务对应的undo log
放入待删除列表中,之后会通过后台的purge thread
对待删除的列表进行删除处理。这里,值得注意的是:undo log
是一种逻辑日志,记录的是一个变化过程。
比如,MySQL
执行一个delete
操作,undo log
就会记录一个insert
操作;MySQL
执行一个insert
操作,undo log
就会记录一个delete
操作;MySQL
执行一个update
操作,undo log
就会记录一个相反的update
操作。
undo log
以段的方式来管理和记录日志信息,在InnoDB
存储引擎的数据文件中,包含了一种叫做rollback segment
的回滚段,其内部包含了1024
个undo log senment
。
2.2 undo log作用
undo log
对于MySQL
实现事务来说,起着至关重要的作用,它实现了事务的原子性和多版本并发控制,也就是我们经常说的MVCC
。
2.2.1 实现事务的原子性
undo log
能够实现MySQL
事务的原子性,在事务的处理过程中,如果MySQL
出现了错误或者用户手动执行了事务的回滚操作(执行了rollback
操作),MySQL
可以利用undo log
日志将数据库中的数据恢复到之前的状态。
2.2.2 实现MVCC机制
undo log
在MySQL
的InnoDB
存储引擎中实现了多版本并发控制(MVCC
)机制。事务未提交前,undo log
保存了未提交之前的版本数据,undo log
中的数据可以作为旧版本数据的副本或者快照以便其他并发事务进行读取操作。
事务A
手动开启事务后,对goods
数据表中id
为1
的数据进行更新操作,首先会把更新命中的数据写入到undo Buffer
中。在事务A
未提交之前,此时,事务B
手动开启事务,对goods
数据表中的id
为1
的数据进行查询操作,此时的事务B
会读取undo log
中的数据并返回给客户端,这就是MySQL
中的MVCC
机制。
可以在MySQL
中通过下面的命令来查看控制undo log
日志的参数。
show variables like '%innodb_undo%';
另外,MVCC
的实现依赖于:隐藏字段、Read View
、undo log
。在内部实现中,InnoDB
通过数据行的DB_TRX_ID
和Read View
来判断数据的可见性,如不可见,则通过数据行的DB_ROLL_PTR
找到undo log
中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建Read View
之前已经提交的修改和该事务本身做的修改。
三、redo log日志
3.1 概念
重做日志(redo log
)又叫事务日志,是InnoDB
存储引擎独有的,它让MySQL
拥有了崩溃恢复能力。
比如MySQL
实例挂了或宕机了,重启时,InnoDB
存储引擎会使用redo log
恢复数据,保证数据的持久性与完整性。
MySQL
中数据是以页为单位,当执行查询时,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到Buffer Pool
中。后续的查询都是先从Buffer Pool
中找,没有命中再去硬盘加载,减少硬盘IO
开销,提升性能。更新表数据的时候,也是如此,发现Buffer Pool
里存在要更新的数据,就直接在Buffer Pool
里更新。
然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(redo log buffer
)里,接着刷盘到redo log
文件里。
理想情况,事务一提交就会进行刷盘操作,但实际上,刷盘的时机是根据策略来进行的。
每条
redo
记录由“表空间号 + 数据页号 + 偏移量 + 修改数据长度 + 具体修改的数据”组成
3.2 刷盘时机
InnoDB
存储引擎为redo log
的刷盘策略提供了innodb_flush_log_at_trx_commit
参数,它支持三种策略:
- 0:表示每次事务提交时不进行刷盘操作
- 1:表示每次事务提交时都将进行刷盘操作(默认值)
- 2:表示每次事务提交时都只把
redo log buffer
内容写入page cache
innodb_flush_log_at_trx_commit
参数默认为1
,也就是说当事务提交时会调用fsync
对redo log
进行刷盘
另外,InnoDB
存储引擎有一个后台线程,每隔1
秒,就会把redo log buffer
中的内容写到文件系统缓存(page cache
),然后调用fsync
刷盘。
也就是说,一个没有提交事务的redo log
记录,也可能会刷盘。
为什么呢?
因为在事务执行过程redo log
记录是会写入redo log buffer
中,这些redo log
记录会被后台线程刷盘。
除了后台线程每秒1
次的轮询操作,还有一种情况,当redo log buffer
占用的空间即将达到innodb_log_buffer_size
一半的时候,后台线程会主动刷盘。
mysql日志操作步骤:
log_buff —mysql写 (write) —> log_file —OS刷新 (flush) —> disk
0(延迟写):log_buff —每隔1秒—> log_file —实时—> disk
1(实时写,实时刷):log_buff —实时—> log_file —实时—> disk
2(实时写,延迟刷):log_buff —实时—> log_file —每隔1秒—> disk
下面是不同刷盘策略的流程图。
3.2.1 延迟写
为0
时,如果MySQL
挂了或宕机可能会有1
秒数据的丢失。
3.2.2 实时写,实时刷
为1
时,只要事务提交成功,redo log
记录就一定在硬盘里,不会有任何数据丢失。
如果事务执行期间MySQL
挂了或宕机,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失。
3.2.3 实时写,延迟刷
为2
时,只要事务提交成功,redo log buffer
中的内容只写入文件系统缓存(page cache
)。
如果仅仅只是MySQL
挂了不会有任何数据丢失,但是宕机可能会有1
秒数据的丢失。
3.3 日志文件组
硬盘上存储的redo log
日志文件不只一个,而是以一个日志文件组的形式出现的,每个的redo
日志文件大小都是一样的。
比如可以配置为一组4
个文件,每个文件的大小是1GB
,整个redo log
日志文件组可以记录4G
的内容。
它采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如下图所示。
在日志文件组中还有两个重要的属性
- write pos是当前记录的位置,一边写一边后移。
- checkpoint是当前要擦除的位置,也是往后推移。
每次刷盘redo log
记录到日志文件组中,write pos
位置就会后移更新。每次MySQL
加载日志文件组恢复数据时,会清空加载过的redo log
记录,并把checkpoint
后移更新。write pos
和checkpoint
之间空着的部分可以用来写入新的redo log
记录。
如果write pos
追上checkpoint
,表示日志文件组满了,这时候不能再写入新的redo log
记录,MySQL
得停下来,清空一些记录,把checkpoint
推进一下。
3.4 小结
只要每次把修改后的数据页直接刷盘不就好了,还有redo log
什么事?
实际上,数据页大小只有16KB
,刷盘比较耗时,可能就修改了数据页里的几Byte
数据,有必要把完整的数据页刷盘吗?而且数据页刷盘是随机写,因为一个数据页对应的位置可能在硬盘文件的随机位置,所以性能是很差。如果是写redo log
,一行记录可能就占几十Byte
,只包含表空间号、数据页号、磁盘文件偏移量、更新值,再加上是顺序写,所以刷盘速度很快。所以用redo log
形式记录修改内容,性能会远远超过刷数据页的方式,这也让数据库的并发能力更强。
其实内存的数据页在一定时机也会刷盘,我们把这称为页合并,讲
Buffer Pool
的时候会对这块细说
四、binlog日志
4.1 概念
redo log
它是物理日志,记录内容是“在某个数据页上做了什么修改”,属于InnoDB
存储引擎。
而binlog
是逻辑日志又叫二进制日志或者归档日志,记录内容是语句的原始逻辑,类似于“给ID=2
这一行的c
字段加1
”,属于MySQL Server
层。
不管用什么存储引擎,只要发生了表数据更新,都会产生binlog
日志。那binlog
到底是用来干嘛的?
可以说MySQL
数据库的数据备份、主备、主主、主从都离不开binlog
,需要依靠binlog
来同步数据,保证数据一致性。
binlog
会记录所有涉及更新数据的逻辑操作,并且是顺序写。
4.2 记录格式
binlog
日志有三种格式,可以通过binlog_format
参数指定。
- statement
- row
- mixed
指定statement
,记录的内容是SQL
语句原文,比如执行一条update T set update_time=now() where id=1
,记录的内容如下。
同步数据时,会执行记录的SQL
语句,但是有个问题,update_time=now()
这里会获取当前系统时间,直接执行会导致与原库的数据不一致。为了解决这种问题,我们需要指定为row
,记录的内容不再是简单的SQL
语句了,还包含操作的具体数据,记录内容如下。
row
格式记录的内容看不到详细信息,要通过mysqlbinlog
工具解析出来。
update_time=now()
变成了具体的时间update_time=1627112756247
,条件后面的@1、@2、@3都是该行数据第1个~3个字段的原始值(假设这张表只有3个字段)。
这样就能保证同步数据的一致性,通常情况下都是指定为row
,这样可以为数据库的恢复与同步带来更好的可靠性。但是这种格式,需要更大的容量来记录,比较占用空间,恢复与同步时会更消耗IO
资源,影响执行速度。所以就有了一种折中的方案,指定为mixed
,记录的内容是前两者的混合。
MySQL
会判断这条SQL
语句是否可能引起数据不一致,如果是,就用row
格式,否则就用statement
格式。
4.3 写入机制
binlog
的写入时机也非常简单,事务执行过程中,先把日志写到binlog cache
,事务提交的时候,再把binlog cache
写到binlog
文件中。因为一个事务的binlog
不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache
。
可以通过binlog_cache_size
参数控制单个线程binlog cache
大小,如果存储内容超过了这个参数,就要暂存到磁盘(Swap
)。
binlog
日志刷盘流程如下
- 上图的
write
,是指把日志写入到文件系统的page cache
,并没有把数据持久化到磁盘,所以速度比较快 - 上图的
fsync
,才是将数据持久化到磁盘的操作
write
和fsync
的时机,可以由参数sync_binlog
控制,默认是0
。为0
的时候,表示每次提交事务都只write
,由系统自行判断什么时候执行fsync
。
虽然性能得到提升,但是机器宕机,page cache
里面的binglog
会丢失。为了安全起见,可以设置为1
,表示每次提交事务都会执行fsync
,就如同binlog日志刷盘流程一样。最后还有一种折中方式,可以设置为N(N>1)
,表示每次提交事务都write
,但累积N
个事务后才fsync
。
在出现IO
瓶颈的场景里,将sync_binlog
设置成一个比较大的值,可以提升性能。同样的,如果机器宕机,会丢失最近N
个事务的binlog
日志。
五、两阶段提交
redo log
(重做日志)让InnoDB
存储引擎拥有了崩溃恢复能力。binlog
(归档日志)保证了MySQL
集群架构的数据一致性。虽然它们都属于持久化的保证,但是则重点不同。
在执行更新语句过程,会记录redo log
与binlog
两块日志,以基本的事务为单位,redo log
在事务执行过程中可以不断写入,而binlog
只有在提交事务时才写入,所以redo log
与binlog
的写入时机不一样。
回到正题,redo log
与binlog
两份日志之间的逻辑不一致,会出现什么问题?
我们以update
语句为例,假设id=2
的记录,字段c
值是0
,把字段c
值更新成1
,SQL
语句为update T set c=1 where id=2
。
假设执行过程中写完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
日志,就会回滚该事务。
再看一个场景,redo log
设置commit
阶段发生异常,那会不会回滚事务呢?
并不会回滚事务,它会执行上图框住的逻辑,虽然redo log
是处于prepare
阶段,但是能通过事务id
找到对应的binlog
日志,所以MySQL
认为是完整的,就会提交事务恢复数据。
六、总结
MySQL
的InnoDB
引擎使用redo log(重做日志)保证事务的持久性,使用undo log(回滚日志)来保证事务的原子性。通过读写锁和MVCC来实现事务的隔离性。MySQL
数据库的数据备份、主备、主主、主从都离不开binlog
,需要依靠binlog
来同步数据,保证数据一致性。