MySQL三大日志详解
redo log(崩溃恢复)
redo log(重做日志)是 InnoDB 存储引擎独有的,它让 MySQL 拥有了崩溃恢复能力。
比如 MySQL 实例挂了或宕机了,重启时,InnoDB 存储引擎会使用 redo log 恢复数据,保证数据的持久性与完整性。
MySQL 中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 Buffer Pool
中。
后续的查询都是先从 Buffer Pool
中找,没有命中再去硬盘加载,减少硬盘 IO 开销,提升性能。
更新表数据的时候,也是如此,发现 Buffer Pool
里存在要更新的数据,就直接在 Buffer Pool
里更新。
然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(redo log buffer
)里,接着刷盘到 redo log 文件里
理想情况,事务一提交就会进行刷盘操作,但实际上,刷盘的时机是根据策略来进行的。
小贴士:每条 redo 记录由“表空间号+数据页号+偏移量+修改数据长度+具体修改的数据”组成
刷盘时机
InnoDB 刷新重做日志的时机有几种情况:
InnoDB 将 redo log 刷到磁盘上有几种情况:
- 事务提交:当事务提交时,log buffer 里的 redo log 会被刷新到磁盘(可以通过
innodb_flush_log_at_trx_commit
参数控制,后文会提到)。 - log buffer 空间不足时:log buffer 中缓存的 redo log 已经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。
- 事务日志缓冲区满:InnoDB 使用一个事务日志缓冲区(transaction log buffer)来暂时存储事务的重做日志条目。当缓冲区满时,会触发日志的刷新,将日志写入磁盘。
- Checkpoint(检查点):InnoDB 定期会执行检查点操作,将内存中的脏数据(已修改但尚未写入磁盘的数据)刷新到磁盘,并且会将相应的重做日志一同刷新,以确保数据的一致性。
- 后台刷新线程:InnoDB 启动了一个后台线程,负责周期性(每隔 1 秒)地将脏页(已修改但尚未写入磁盘的数据页)刷新到磁盘,并将相关的重做日志一同刷新。
- 正常关闭服务器:MySQL 关闭的时候,redo log 都会刷入到磁盘里去。
总之,InnoDB 在多种情况下会刷新重做日志,以保证数据的持久性和一致性。
我们要注意设置正确的刷盘策略innodb_flush_log_at_trx_commit
。根据 MySQL 配置的刷盘策略的不同,MySQL 宕机之后可能会存在轻微的数据丢失问题。
innodb_flush_log_at_trx_commit
的值有 3 种,也就是共有 3 种刷盘策略:
- 0:设置为 0 的时候,表示每次事务提交时不进行刷盘操作。这种方式性能最高,但是也最不安全,因为如果 MySQL 挂了或宕机了,可能会丢失最近 1 秒内的事务。
- 1:设置为 1 的时候,表示每次事务提交时都将进行刷盘操作。这种方式性能最低,但是也最安全,因为只要事务提交成功,redo log 记录就一定在磁盘里,不会有任何数据丢失。
- 2:设置为 2 的时候,表示每次事务提交时都只把 log buffer 里的 redo log 内容写入 page cache(文件系统缓存)。page cache 是专门用来缓存文件的,这里被缓存的文件就是 redo log 文件。这种方式的性能和安全性都介于前两者中间。
刷盘策略innodb_flush_log_at_trx_commit
的默认值为 1,设置为 1 的时候才不会丢失任何数据。为了保证事务的持久性,我们必须将其设置为 1。
另外,InnoDB 存储引擎有一个后台线程,每隔1
秒,就会把 redo log buffer
中的内容写到文件系统缓存(page cache
),然后调用 fsync
刷盘。
也就是说,一个没有提交事务的 redo log 记录,也可能会刷盘。
为什么呢?
因为在事务执行过程 redo log 记录是会写入redo log buffer
中,这些 redo log 记录会被后台线程刷盘。
redo log 小结
相信大家都知道 redo log 的作用和它的刷盘时机、存储形式。
现在我们来思考一个问题:只要每次把修改后的数据页直接刷盘不就好了,还有 redo log 什么事?
它们不都是刷盘么?差别在哪里?
实际上,**数据页大小是16KB
**,刷盘比较耗时,可能就修改了数据页里的几 Byte
数据,有必要把完整的数据页刷盘吗?
而且数据页刷盘是随机写,因为一个数据页对应的位置可能在硬盘文件的随机位置,所以性能是很差。
如果是写 redo log,一行记录可能就占几十 Byte
,只包含表空间号、数据页号、磁盘文件偏移
量、更新值,再加上是顺序写,所以刷盘速度很快。
所以用 redo log 形式记录修改内容,性能会远远超过刷数据页的方式,这也让数据库的并发能力更强。
其实内存的数据页在一定时机也会刷盘,我们把这称为页合并,讲
Buffer Pool
的时候会对这块细说
1 | 1 Byte = 8 bit |
binlog(主从同步)
Redo log 它是物理日志,记录内容是“在某个数据页上做了什么修改”,属于 InnoDB 存储引擎。
而 binlog 是逻辑日志,记录内容是语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”,属于MySQL Server
层。
不管用什么存储引擎,只要发生了表数据更新,都会产生 binlog 日志。
那 binlog 到底是用来干嘛的?
可以说 MySQL 数据库的数据备份、主备、主主、主从都离不开 binlog,需要依靠 binlog 来同步数据,保证数据一致性
binlog 会记录所有涉及更新数据的逻辑操作,并且是顺序写
记录格式
binlog 日志有三种格式,可以通过binlog_format
参数指定。
- statement(基于 SQL 语句的复制)
- 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
格式
写入机制
binlog 的写入时机也非常简单,事务执行过程中,先把日志写到binlog cache
,事务提交的时候,再把binlog cache
写到 binlog 文件中。
因为一个事务的 binlog 不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache
。
我们可以通过binlog_cache_size
参数控制单个线程 binlog cache 大小,如果存储内容超过了这个参数,就要暂存到磁盘(Swap
)。
binlog 日志刷盘流程如下
- 上图的 write,是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,所以速度比较快
- 上图的 fsync,才是将数据持久化到磁盘的操作
write
和fsync
的时机,可以由参数sync_binlog
控制,默认是1
。
为0
的时候,表示每次提交事务都只write
,由系统自行判断什么时候执行fsync
虽然性能得到提升,但是机器宕机,page cache
里面的 binlog 会丢失。
为了安全起见,可以设置为1
,表示每次提交事务都会执行fsync
,就如同 redo log 日志刷盘流程 一样。
最后还有一种折中方式,可以设置为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 认为是完整的,就会提交事务恢复数据
- redo log处于prepare阶段并且binlog中没有记录,此时会进行事务回滚
- redo log处于prepare阶段并且binglog中有记录,此时不会进行事务回滚,mysql认为数据正常
undo log(回滚)
存储的内容:
Undo log 中存储的信息主要包括:
- 旧值: 在事务开始之前数据项的旧值。
- 操作类型: 对数据项执行的具体操作类型(如INSERT、UPDATE或DELETE)。
- 事务ID: 执行该操作的事务标识符。
- 回滚指针: 指向下一个undo log记录的位置,以便于在回滚操作时可以按照正确的顺序逆向恢复数据。
存储时机:
Undo log 的记录会在事务提交之前进行。具体来说:
- 修改操作: 当一个事务对数据库中的数据进行了修改(例如UPDATE或DELETE操作),系统会在修改数据之前先记录修改前的数据状态到undo log中。
- 插入操作: 对于INSERT操作,虽然没有修改前的数据状态,但仍然会记录事务ID和其他相关信息。
- 提交事务: 事务提交时,undo log 已经包含了足够的信息来支持回滚操作。
数据格式:
Undo log 的具体数据格式依赖于具体的数据库管理系统。不同的DBMS可能有不同的实现方式,但一般而言,undo log 包括以下几个部分:
- 头部信息: 包含记录的类型、事务ID等元数据。
- 数据段: 包含被修改的数据项的旧值。
- 回滚指针: 指向下一条undo log记录的位置。
作用:
Undo log 的主要作用包括:
- 支持回滚: 如果事务在执行过程中遇到错误或被显式回滚,系统可以通过undo log 恢复到事务开始前的状态。
- 读取一致性: 在多版本并发控制(MVCC, Multi-Version Concurrency Control)中,undo log 可以用来生成在某个时间点的数据库快照,以保证不同事务之间的读取一致性。
具体的结构:
每一个事务对数据的修改都会被记录到 undo log ,当执行事务过程中出现错误或者需要执行回滚操作的话,MySQL 可以利用 undo log 将数据恢复到事务开始之前的状态。
undo log 属于逻辑日志,记录的是 SQL 语句,比如说事务执行一条 DELETE 语句,那 undo log 就会记录一条相对应的 INSERT 语句。同时,undo log 的信息也会被记录到 redo log 中,因为 undo log 也要实现持久性保护。并且,undo-log 本身是会被删除清理的,例如 INSERT 操作,在事务提交之后就可以清除掉了;UPDATE/DELETE 操作在事务提交不会立即删除,会加入 history list,由后台线程 purge 进行清理。
undo log 是采用 segment(段)的方式来记录的,每个 undo 操作在记录的时候占用一个 undo log segment(undo 日志段),undo log segment 包含在 rollback segment(回滚段)中。事务开始时,需要为其分配一个 rollback segment。每个 rollback segment 有 1024 个 undo log segment,这有助于管理多个并发事务的回滚需求。
通常情况下, rollback segment header(通常在回滚段的第一个页)负责管理 rollback segment。rollback segment header 是 rollback segment 的一部分,通常在回滚段的第一个页。history list 是 rollback segment header 的一部分,它的主要作用是记录所有已经提交但还没有被清理(purge)的事务的 undo log。这个列表使得 purge 线程能够找到并清理那些不再需要的 undo log 记录。
另外,MVCC
的实现依赖于:隐藏字段、Read View、undo log。在内部实现中,InnoDB 通过数据行的 creator_trx_id
和 Read View
来判断数据的可见性,如不可见,则通过数据行的 DB_ROLL_PTR
找到 undo log 中的历史版本。每个事务读到的数据版本可能是不一样的,在同一个事务中,用户只能看到该事务创建 Read View
之前已经提交的修改和该事务本身做的修改
总结
MySQL InnoDB 引擎使用 redo log(重做日志) 保证事务的持久性,使用 undo log(回滚日志) 来保证事务的原子性。
MySQL 数据库的数据备份、主备、主主、主从都离不开 binlog,需要依靠 binlog 来同步数据,保证数据一致性。
- redo log(重做日志):保证事务的持久性
- binlog:保证数据一致性
- undo log(回滚日志):保证事务的原子性
三者执行顺序:redo log、undo log、bin log
在事务执行过程中,先记录 redo log 和 undo log;在事务提交时,先刷盘 redo log,再记录 binlog,最后删除 undo log;在事务回滚时,根据 undo log 进行数据恢复。