MySQL中的 redo 日志文件
MySQL中有三种日志文件,redo log、bin log、undo log。redo log 是 存储引擎层(innodb)生成的日志,主要为了保证数据的可靠性;bin log 是 MySQL 数据库层面上生成的日志,主要用于 point in time 恢复和主从复制。undo log 主要用于事务的回滚(undo log 记录的是每个修改操作的逆操作) 和 一致性非锁定读(undo log 回滚行记录到某种特定的版本---MVCC 多版本并发控制)。
redo log 和 undo log 都是存储引擎层面上生成的日志,并且都记录了数据的修改:只不过 redo log记录的是"物理级别"上的页修改操作,比如页号xxx、偏移量yyy写入了'zzz'数据;而undo log 记录的是逻辑操作日志,比如对某一行数据进行了INSERT语句操作,那么 undo log就记录一条与之相反的DELETE操作。
redo log 作用---保证事务的持久性
MySQL作为一个存储系统,为了保证数据的可靠性,最终得落盘。但是,又为了数据写入的速度,需要引入基于内存的"缓冲池"。其实不止MySQL,这种引入缓冲来解决速度问题的思想无处不在。既然数据是先缓存在缓冲池中,然后再以某种方式刷新到磁盘,那么就存在因宕机导致的缓冲池中的数据丢失,为了解决这种情况下的数据丢失问题,引入了redo log。在其他存储系统,比如Elasticsearch中,也有类似的机制,叫translog。
但是一般讨论数据写入时,在MySQL中,一般叫事务操作,根据事务的ACID特性,如何保证一个事务提交后Durability的保证?而这就是 redo log 的作用。当向MySQL写用户数据时,先写redo log,然后redo log根据"某种方式"持久化到磁盘,变成redo log file,用户数据则在"buffer"中(比如数据页、索引页)。如果发生宕机,则读取磁盘上的 redo log file 进行数据的恢复。从这个角度来说,MySQL 事务的持久性是通过 redo log 来实现的。
redo log 写入磁盘
在事务运行过程中,会不断地产生 redo log,这些 redo log 会先定入 redo log buffer中,然后再将 redo log buffer 中的数据以某些方式顺序地写入到磁盘(各个操作的redo log 汇总到 redo log buffer,再由 redo log buffer 统一写磁盘,就能做到顺序写了)。这些方式有:
MySQL master 线程周期性任务 每秒一次,将 redo log buffer 刷新到磁盘(即使这个事务尚未提交)
MySQL master 线程周期性任务 每10秒一次,将 redo log buffer 刷新到磁盘
当redo log buffer size 剩余空间小于1/2时(innodb_log_buffer_size参数),将 redo log buffer 刷新到磁盘
当 redo log file 大小已经达到某个域值快要"不可用"时(日志文件组轮流写文件),触发 async/sync flush checkpoint,及时将一些脏页刷新到磁盘,并同时将redo log buffer刷新到磁盘,然后更新redo log file 相应的 log sequence number值。
redo log buffer 的刷新到磁盘的时机由参数 innodb_flush_log_at_trx_commit 参数控制,可取的值有:0、1、2:
0 由master 线程周期性任务刷新
1 在事务 commit 时 redo log buffer 同步写入 disk,伴随 fsync 调用
2 将 redo log 日志数据异步写入 disk,即只写到文件系统缓存中,在事务成功提交后并不能保证 redo log 数据一定存储到磁盘上了
redo log 写入磁盘时,是以扇区大小512B写入的,保证每次写都能写入成功,不需要有 double write 机制。
redo log buffer 与数据页、索引页、何时刷盘的区别?
Innodb存储引擎基于缓冲池来平衡CPU与磁盘之间的速度差距,提高数据库的性能。数据页、索引页属于缓冲池的一部分,而redo log buffer其实也是一块内存结构。redo log 的作用是用于故障恢复,实现事务的持久性机制,MySQL宕机了,从 redo log 文件中读取数据进行恢复。所以我们看到:innodb_flush_log_at_trx_commit=1时,每当事务提交时,就会记录一些信息到 redo log buffer,并且 fsync 写磁盘。事务提交,修改了很多用户数据,这些用户数据是在数据页、索引页 这样的缓冲池中,这些页是通过LRU算法来管理的(LRU List、Free List、Flush List),这些页的刷盘是由checkpoint机制来管理的,不要与redo log buffer刷盘策略混淆。
说起checkpoint,一般都会提到:WAL(write ahead log)即:当事务提交时,先写redo log,再修改页(这里的页,就是缓冲池中的数据页、索引页)。 为什么?就是因为redo log 可以做到:每次写redo log 时,可以同步写入disk(伴随fsync调用),也就是innodb_flush_log_at_trx_commit参数设置为1的情形。 而我们缓冲池中的数据页、索引页是没法做到,每修改一个页就fsync刷新磁盘的,原因是:
倘若每次一个页发生变化,就将新页的版本刷新到磁盘,那么这个开销是很大的。同时,从缓冲池将页的新版本刷新到磁盘时发生了宕机,那么数据就不能恢复了。
这个时候,你可能会反问?那难道:每写一次redo log就刷新一次磁盘,难道开销就很小了?我的理解是: 数据页、索引页的刷盘是不容易的,因为底层是一棵B+树结构,数据页的刷盘要做到磁盘的顺序写,是要很多优化技巧的。而redo log的格式,是固定的,MySQL定义了redo log日志的各个字段,再通过redo log buffer,很容易做到顺序写刷盘。并且,相比于数据页、索引页的中的内容,redo log的内容要简单得多。
另外,还想解释一下前文的那句话,为什么说:从缓冲池将页的新版本刷新到磁盘时发生了宕机,那么数据就不能恢复了? MySQL针对数据页的写入有一个:double write 机制(双写),因为数据页大小是16KB,如果一个数据页只写了前面4KB就宕机,就是部分写失效。前面提到 redo log记录的是物理级别上的页操作,而现在这个页只写了4KB,本身就是一个有"故障"的页,那么 redo log 对这个页的写操作的记录本身就是错误的。因此,就有了double write:数据页先copy到double write buffer,先将数据页顺序写共享表空间,然后再写一份到该数据所对应的表空间中。如下图所示:
而 redo log写入磁盘时,是按512个字节(一个磁盘扇区大小)写入的,扇区是写入的最小单位,因此可以保证写入是必定成功的,不需要 double write buffer。
其实,写到这里:解决了我心里很久的一个疑问,程序产生的数据都是先在内存里面,然后从内存里面持久化到磁盘的。如果进程突然挂了,为什么说数据还能不丢失呢?想想事务的持久性:只要事务提交了,那么产生的结果就是永久性的,就算MySQL进程被突然kill了,也能恢复。
不管是MySQL还是ES,都有checkpoint机制,当服务进程挂掉时,从redo log 或者translog上恢复数据。当发生故障时,到底要从redo log的哪个位置处开始恢复呢?它们都有一个叫做LSN(log sequence number)检查点的东西,它表示在LSN之前的数据(缓冲池中的数据页、索引页)都已经持久化到磁盘了,而LSN之后的数据,尚未来得及fsync到磁盘,因此发生故障时需要读取LSN之后的 redo log日志文件进行恢复。
最后留一个思考,关于存储系统的可靠性与可用性。同样作为存储系统,ElasticSearch里面有Translog,为什么Kafka里面没有类似于redo log的保证数据可靠性机制呢? ES也是分布式多副本,副本的复制也是基于PacificA算法,但是ES中有Translog用来进行数据恢复,为什么Kafka没有?想想涉及 Kafka Producer写入 数据可靠性保证机制2个参数:ack=all 和 min.insync.replica,再看看 ES里面的2个参数 wait_for_active_shards 和 index.translog.durability 就能知道一些原因了。(wait_for_active_shards参数 涉及到"竞态条件"check-and-then 它并不是原子的)
参考:《MySQL技术内幕》