MySQL的日志
2022-03-02
redoLog 是属于InnoDB引擎所特有的日志,而MySQL Server也有自己的日志,我们一般称为binlog。而undolog用于事务回滚以及实现MVCC。这篇文章主要介绍这三者在Mysql中起到的作用以及其设计的意义。
redolog(重做日志)
redolog是InnoDB引擎实现的日志系统,这里运用到了WAL技术。
WAL,全称是Write-Ahead Logging, 预写日志系统。指的是 MySQL 的写操作并不是立刻更新到磁盘上,而是先记录在日志上,然后在合适的时间再更新到磁盘上。写数据库涉及到树的节点查找(本质是磁盘随机写),远比顺序写日志要慢,因此为了追求速度,数据库在数据被修改时,先写日志,再等待合适时机回写数据到数据库,这样在保证数据不丢的同时最大化了数据库写速度。
这里记redolog的操作,为了更好是性能(虽然顺序写磁盘已经很快了,但是追求速度上仍可继续提升),会先把relog的内容写入redolog ring buffer(内存写),再按照一定策略同步到OS层的buffer,OS Buffer再根据一定策略刷日志到磁盘落地。也就是我们要把用户空间的数据写到磁盘,其实会经过两个buffer的缓存和写策略。
用户空间(user space)下的缓冲区数据一般情况下是无法直接写入磁盘的,中间必须经过操作系统内核空间(kernel space)缓冲区(OS Buffer)。因此,redo log buffer写入redo log file实际上是先写入OS Buffer,然后再通过系统调用fsync()将其刷到redo log file中。
redolog是固定大小,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么redolog总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,所以可以看到因为有这个循环的特性,我们可以把relog写空间抽象为一个环空间,当数据写满整个环空间时,需要停下写操作,进行数据擦除。如下面这个图所示。
write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件,上图的绿色部分表示redolog空闲空间,支持记录数据。如果 write pos 追上 checkpoint,redolog写满了,这时候不能再执行新的更新,必须得停下来先擦掉一些记录,把 checkpoint 推进一下,即刷数据脏页到磁盘。这就暴露了一个问题,如果redolog文件大小设置太小,就容易造成Mysql经常需要停止更新操作转而来做写盘擦除redolog的操作,Mysql写性能急速下降。
这个checkpoint机制就是为了数据库的crash-safe,也就是数据库宕机后,重启数据库可以保证数据不丢失。重启数据库后,首先检查redolog的checkpoint,我们就知道我们数据库更新的操作在宕机前我们更新了哪些数据到数据库中,此时我们从write-pos继续更新数据即可,进而redolog即可恢复到数据库崩溃前的状态,保证数据不丢失。
这里提到合适的时机才把数据写到磁盘上,具体的时机是怎么设计的呢?
我们可以配置innodb_flush_log_at_trx_commit这个参数来控制redolog写入磁盘的时机,该参数可以控制将 redolog buffer 中的更新记录写入到日志文件以及将日志文件数据刷新到磁盘的操作时机。通过调整这个参数,可以在性能和数据安全之间做取舍。:
- 0:在事务提交时,innodb 不会立即触发将缓存日志写到磁盘文件的操作,而是每秒触发一次缓存日志(redo buffer)回写磁盘操作,并调用系统函数 fsync 刷新 IO 缓存。这种方式效率最高,也最不安全。
- 1:在每个事务提交时,innodb 立即将缓存(redo buffer)中的 redo 日志回写到日志文件,并调用 fsync 刷新 IO 缓存。
- 2:在每个事务提交时,innodb 立即将缓存(redo buffer)中的 redo 日志回写到日志文件,但并不马上调用 fsync 来刷新 IO 缓存,而是每秒只做一次磁盘IO 缓存刷新操作。只要操作系统不发生崩溃,数据就不会丢失,这种方式是对性能和数据安全的折中,其性能和数据安全性介于其他两种方式之间。
innodb_flush_log_at_trx_commit 参数的默认值是 1,即每个事务提交时都会从 log buffer 写更新记录到日志文件,而且会实际刷新磁盘缓存,显然,这完全能满足事务的持久化要求,是最安全的,但这样会有较大的性能损失。我们工业环境上该参数设置为1。
binlog(归档日志)
binlog日志有两个最重要的使用场景:
- mysql主从复制:mysql replication在master端开启binlog,master把它的binlog日志传递给slaves来达到master-slave数据一致的目的。
- 数据恢复:通过mysqlbinlog工具来恢复数据。
跟redolog的innodb_flush_log_at_trx_commit参数类似,参数sync_binlog表示每写缓冲多少次就同步到磁盘,这个参数可以让用户在性能和数据安全上自行选择:
- N=1:表示采用同步写磁盘的方式来写二进制日志,这时写操作不使用操作系统的缓冲来写二进制日志,每次事务提交都会写入文件,即调用fsync()。
- N=0:表示MySQL不控制binlog的刷新,由文件系统自己控制它的缓存的刷新。这时候的性能是最好的,但是风险也是最大的。因为一旦OS系统Crash,在binlog_cache中的所有binlog信息都会被丢失。
我们工业环境上该参数设置为1。
最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。
这两种日志有以下几点不同。
- redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。
- redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。
- redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
- redo log具有checkpoint机制,具有crash-safe能力,binlog没有。
- binlog可以恢复DB到任意一个时刻的数据库状态,但不保证能恢复到最新的数据库状态,比如数据库crash后重启,单靠binlog恢复,可能导致丢失一些数据。而redolog只能恢复到当前最新数据库状态。
- redo log是数据库对数据的保护,就算不断的crash而不影响已成功写入的,持久性。binlog可以理解为备份,用于恢复某段时间内的数据
执行一条update SQL语句的流程,这里就是经典的分布式事务流程-两阶段提交:
- 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。(server层->引擎层->Server层)
- 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。(server层->引擎层)
- 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。(引擎层)
- 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。(server层)
- 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。(引擎层)
这里讨论二阶段提交的重要性,也就是最后的commit操作是否是必要的呢?我只写binlog和redolog是否也可以实现crash-safe?
- 先写redo log后写binlog,此时redolog已经写完,MySQL此时crash,binlog写失败,Mysql重启后,redolog进行数据恢复,Db里有这个崩溃前的数据,而binlog没有这条语句,因此逻辑上与原库不一致,少了一条数据。
- 先写binlog后写redolog,binlog写完,Mysql crash,redolog写失败,重启恢复后,发现数据库有没有这个更新的数据,但是逻辑上binlog却有这条写成功的记录,因此也与原库不一样,多了一条数据。
redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。
两阶段提交是跨系统维持数据逻辑一致性时常用的一个方案。其实把Mysql的两阶段提交也可以看成两个分布式服务处理两个不同事情,redo log在Innodb引擎内操作的,binlog是在server层操作的,我们就可以把引擎层和server层看成两个分布式服务,那他们要分别进行两个相关联的操作,就意味着要实现分布式事务,而两阶段提交,就是其中的一种解决方案。
崩溃恢复分析:
两阶段提交
- prepare阶段写redolog
- 写binlog
- commit
- 当在2之前崩溃时:重启恢复:发现没有commit,回滚。备份恢复:没有binlog 。
一致 - 当在3之前崩溃:
重启恢复:虽没有commit,但满足prepare和binlog完整,所以重启后会自动commit。备份:有binlog. 一致
我们怎么判断Mysql binlog是完整的?
- statement 格式的 binlog,最后会有 COMMIT;
- row 格式的 binlog,最后会有一个 XID event。
- binlog-checksum 参数,用来验证 binlog 内容的正确性.
redolog跟binlog是怎么关联在一起?
它们有一个共同的数据字段,叫 XID。崩溃恢复的时候,会按顺序扫描 redo log
如果碰到既有 prepare、又有 commit 的 redo log,就直接提交;如果碰到只有 parepare、而没有 commit 的 redo log,就拿着 XID 去 binlog 找对应的事务。
undolog(回滚日志)
undolog是逻辑日志,主要记录数据的逻辑变化,为了在发生错误时回滚之前的操作,需要将之前的操作都记录下来,然后在发生错误时才可以回滚。体现两个作用:
- 事务回滚
- MVCC
undo日志,只将数据库逻辑地恢复到原来的样子,在回滚的时候,它实际上是做的相反的工作,比如一条INSERT ,对应一条 DELETE,对于每个UPDATE,对应一条相反的 UPDATE,将修改前的行放回去。通过undo log进行事务回滚操作可以保障事务的原子性。
在InnoDB存储引擎中,undo log分为 insert undolog和update undolog。
以一个例子介绍undolog写入时机:
假设有A、B两个数据,值分别为1,2.
1. 事务开始
2. 记录A=1到undo log -- 因为经过分析知道这是一个update操作,先记录update前A的值
3. 修改A=3
4. 记录A=3到 redo log
5. 记录B=2到 undo log --因为经过分析知道这是一个update操作,先记录update前B的值
6. 修改B=4
7. 记录B=4到redo log
8. 记录binlog
9. 事务提交