一、有哪些日志
MySQL应该是我们用的最多,也算是最熟悉的数据库了。那么,MySQL中有哪些日志了,或者你知道的有哪些日志了?
首先,我们能接触到的,一般我们排查慢查询时,会去看慢查询日志。如果做过数据备份会恢复的,可能接触或用过BinLog。那还有其他的吗?对MySQL原理比较了解的,应该知道还有Redo Log和Undo Log。这些都是比较常见:慢查询日志、Binlog、Redo Log、Undo Log。
其实,MySQL中还有:错误日志(errorlog)、一般查询日志(general log)、中继日志(relay log),只是这些大家接触少点。
总结下,MySQL中一共七种日志,分别为:慢查询日志、Binlog、Redo Log、Undo Log、错误日志(errorlog)、一般查询日志(general log)、中继日志(relay log)。
本文,我们重点看下Redo Log、Undo Log以及Binlog。
二、更新语句执行
首先,我们了解下,一条更新语句是如何执行的,在更新的插入的过程中,各个日志的写入顺序是怎样的。MySQL的更新语句执行过程如下图所示:
一条更新语句的执行过程为:
1、MySQL执行器负责执行,首先查询待更新的记录:
1)如果在Buffer Pool中,那么返回这条记录;
2)如果没在,那么查询数据,加载进Buffer Pool;
2、开启事务,首先记录Undo Log;
3、InnoDB 层开始更新记录,会先更新内存(同时标记为脏页),然后将记录写到 redo log 里面;
4、然后开始记录该语句对应的 binlog,此时记录的 binlog 会被保存到 binlog cache;
5、事务提交。采用两阶段的方式:
1)prepare 阶段:将 redo log 对应的事务状态设置为 prepare,然后将 redo log 刷新到硬盘;
2)commit 阶段:将 binlog 刷新到磁盘,接着调用引擎的提交事务接口,将 redo log 状态设置为 commit(将事务设置为 commit 状态后,刷入到磁盘 redo log 文件);
三、Redo日志
2.1、为什么要Redo日志?
对于数据库,一般具有ACID四个特性,这里,我们关注的是持久性,那么如何保证数据库的持久性了?
简单直接一点,就是每次新增更新数据,我们都直接刷到磁盘上就好了,只要刷到了磁盘上,那么持久性一般就得到了保障。但是,这里会有一个问题,刷盘是一个速度比较慢的过程,尤其这种随机IO的场景,如果我们每次更新都直接刷盘,那么性能肯定不高。那么如何提高性能了?
可以先把数据刷到缓冲区中,然后再从缓冲区中刷到磁盘中(可以是个异步的过程),这样写的性能就提上去了。但是了,此时会出现这样的情况,缓冲区中的数据还没有刷到盘中,但是系统突然崩了,那么数据就丢了。那么怎么做到既保证性能,又能保障持久性了?
这就是WAL技术。WAL的技术的核心思想就是:按照顺序IO先把操作记录下来,这样当系统崩溃了,没有刷到磁盘的数据,也可以根据日志回复回来。当前WAL已经应用到各数据库中,比如sqlite、postgresql、etcd、hbase、zookeeper、elasticsearch等,当然MySQL也应用了WAL。
MySQL中WAL的实现,主要是靠Redo日志实现的。详细描述一下:为了取得更好的读写性能,修改Page之前需要先将修改的内容记录到Redo中,并保证Rdeo Log早于对应的Page落盘。InnoDB将修改数据缓存在内存中,当故障发生导致内存数据丢失后,InnoDB会在重启时,通过重放REDO,将Page恢复到崩溃前的状态。
2.2、Redo日志样子
那么Redo日志记录了些什么了,可以做到Crash Recovery?其实我们可以大致猜一下,Redo日志记录的是对于Page的修改,那么需要记录的信息大致为:对那个PAGE,什么位置,做了什么修改。真实的情况,远比这个复杂。到MySQL 8.0为止,已经有多达65种的REDO记录。用来记录这不同的信息,恢复时需要判断不同的REDO类型,来做对应的解析。根据REDO记录不同的作用对象,可以将这65中REDO划分为三个大类:作用于Page,作用于Space,以及提供额外信息的Logic类型。
2.2.1、作用于Page的Redo
这类REDO占所有REDO类型的绝大多数,根据作用的Page的不同类型又可以细分为,Index Page REDO,Undo Page REDO,Rtree PageREDO等。比如MLOG_REC_INSERT,MLOG_REC_UPDATE_IN_PLACE,MLOG_REC_DELETE三种类型分别对应于Page中记录的插入,修改以及删除。这里还是以MLOG_REC_UPDATE_IN_PLACE为例来看看其中具体的内容:
其中,Type就是MLOG_REC_UPDATE_IN_PLACE类型,Space ID和Page Number唯一标识一个Page页,这三项是所有REDO记录都需要有的头信息,后面的是MLOG_REC_UPDATE_IN_PLACE类型独有的,其中Record Offset用给出要修改的记录在Page中的位置偏移,Update Field Count说明记录里有几个Field要修改,紧接着对每个Field给出了Field编号(Field Number),数据长度(Field Data Length)以及数据(Filed Data)。
2.2.2、作用于Space的Redo
这类REDO针对一个Space文件的修改,如MLOG_FILE_CREATE,MLOG_FILE_DELETE,MLOG_FILE_RENAME分别对应对一个Space的创建,删除以及重命名。由于文件操作的REDO是在文件操作结束后才记录的,因此在恢复的过程中看到这类日志时,说明文件操作已经成功,因此在恢复过程中大多只是做对文件状态的检查,以MLOG_FILE_CREATE来看看其中记录的内容:
同样的前三个字段还是Type,Space ID和Page Number,由于是针对Page的操作,这里的Page Number永远是0。在此之后记录了创建的文件flag以及文件名,用作重启恢复时的检查。
2.2.3、提供额外信息的Logic REDO
除了上述类型外,还有少数的几个REDO类型不涉及具体的数据修改,只是为了记录一些需要的信息,比如最常见的MLOG_MULTI_REC_END就是为了标识一个REDO组,也就是一个完整的原子操作的结束。
详细的介绍,可以看下这片文章:庖丁解InnoDB之REDO LOG
2.3、Redo写入流程
对于Redo日志的写入,我们需要考虑点在于:写入效率、存储开销。
2.3.1、写入效率问题
首先我们探讨下写入效率问题。对于写入效率问题,大致的解决思路就是:批量写、并发写、异步写、顺序写等等。那么Redo采用哪些措施来提高效率呢?
异步批量角度:先写入redo log buffer中,通过innodb_flush_log_at_trx_commit参数控制写入策略。如果innodb_flush_log_at_trx_commit=0,则继续停留在redo log buffer中,等待后台定时刷盘;如果innodb_flush_log_at_trx_commit=1,则提交后马上刷盘;如果innodb_flush_log_at_trx_commit=2,会进行写盘(写入文件系统的page cache中),等待后台定时刷盘。
并发的角度:高并发的环境中,会同时有非常多的min-transaction(mtr)需要拷贝数据到Log Buffer,如果通过锁互斥,那么毫无疑问这里将成为明显的性能瓶颈。为此,从MySQL 8.0开始,设计了一套无锁的写log机制,其核心思路是允许不同的mtr,同时并发地写Log Buffer的不同位置。
详细的介绍,可以看下这片文章:庖丁解InnoDB之REDO LOG
2.3.2、存储开销问题
Redo日志怎么存储呢?最简单的我们可以全量存储,但是是否有必要了,其实没必要,因为已经持久化的数据,我们就不需要了。那么可以这样,持久化的数据,就删除,节省空间。这样看起来没问题,但是删除数据,其实也是个不小的开销,尤其是大量存储空间,如果空间可以复用,那么就不会有这个问题了,也避免了删除的开销。
那么怎么实现复用呢?复用其实就是一个循环的概念,走到尾了,又从头开始。这里,需要解决的是 ,不能把还没有持久化的数据给覆盖了,怎么解决了,那就是如果要覆盖,就不能超过持久化那个标记点。如果没有空间,就主要触发数据持久化,那么把这个标记点往前推进,腾出空间。这样就解决这个问题了。
Redo Log存储其实就是这样的,它会在一个环形日志空间上写,写满了,又从头开始。上面说的那个标记点,就是checkpoint。
图中的:
- write pos 和 checkpoint 的移动都是顺时针方向;
- write pos ~ checkpoint 之间的部分(图中的红色部分),用来记录新的更新操作;
- check point ~ write pos 之间的部分(图中蓝色部分):待落盘的脏数据页记录;
如果 write pos 追上了 checkpoint,就意味着 redo log 文件满了,这时 MySQL 不能再执行新的更新操作,也就是说 MySQL 会被阻塞(因此所以针对并发量大的系统,适当设置 redo log 的文件大小非常重要),此时会停下来将 Buffer Pool 中的脏页刷新到磁盘中,然后标记 redo log 哪些记录可以被擦除,接着对旧的 redo log 记录进行擦除,等擦除完旧记录腾出了空间,checkpoint 就会往后移动(图中顺时针),然后 MySQL 恢复正常运行,继续执行新的更新操作。
所以,一次 checkpoint 的过程就是脏页刷新到磁盘中变成干净页,然后标记 redo log 哪些记录可以被覆盖的过程。
2.4、Crash Recovery
如果InnoDB宕机了,数据是怎么恢复的了?了解MySQL的都知道,使用Redo Log恢复的,具体是如何恢复的?除了Redo Log还有其他日志参与吗?比如Binlog或Undo log会参与吗?
InnoDB的数据恢复是一个很复杂的过程,在其恢复过程中,需要redo log、binlog、undo log等参与,这里把InnoDB的恢复过程主要划分为两个阶段:
1、第一阶段主要依赖于redo log的恢复;
2、而第二阶段,恰恰需要binlog和undo log的共同参与。
2.4.1、恢复第一阶段
第一阶段,数据库启动后,InnoDB会通过redo log找到最近一次checkpoint的位置,然后根据checkpoint相对应的LSN开始,获取需要重做的日志,接着解析获取的日志并且保存到一个哈希表中,最后通过遍历哈希表中的redo log信息,读取相关页进行恢复。redo log全部被解析并且apply完成,整个InnoDB recovery的第一阶段也就结束了,在该阶段中,所有已经被记录到redo log但是没有完成数据刷盘的记录都被重新落盘。
然而,InnoDB单靠redo log的恢复是不够的,这样还是有可能会丢失数据(或者说造成主从数据不一致),因为在事务提交过程中,写binlog和写redo log提交是两个过程,写binlog在前而redo提交在后,如果MySQL写完binlog后,在redo提交之前发生了宕机,这样就会出现问题:binlog中已经包含了该条记录,而redo没有持久化。binlog已经落盘就意味着slave上可以apply该条数据,redo没有持久化则代表了master上该条数据并没有落盘,也不能通过redo进行恢复。这样就造成了主从数据的不一致,换句话说主上丢失了部分数据,那么MySQL又是如何保证在这样的情况下,数据还是一致的?这就需要进行第二阶段恢复。
2.4.2、恢复第二阶段
第二阶段,如果Binlog已经写完,但是Redo Log还没Commit,此时该怎么办呢?分析下,此时有两种情况:
1)如果Binlog中有当前事务的XID,则说明Redo已经完成写入,知识还没更改状态而已,那么提交事务就好;
2)如果Binlog中没有当前事务的XID,那么说明Redo完成写入,但是Binlog没有刷盘,则回滚事务;
详细介绍,可以看:
MySQL InnoDB Recovery过程解析
MySQL · 引擎特性 · InnoDB 崩溃恢复过程
四、Undo 日志
3.1、为什么要Undo Log
3.1.1、 事务回滚
数据库可能在任何时刻,由于如硬件故障,软件Bug,运维操作等原因突然崩溃。这个时候尚未完成提交的事务可能已经有部分数据写入了磁盘,如果不加处理,会违反数据库对Atomic的保证,也就是任何事务的修改要么全部提交,要么全部取消。
针对这个问题,解法之一,就是在没有真正完成这个事务时,不要干扰正常的数据。比如单独copy一份相关数据,在上面进行修改,如果需要回滚就丢弃,如果执行成功就Merge或替换。这种方式,JIM GRAY等人在1981年就提出了,在《The Recovery Manager of the System R Database Manager》中提出了Shadow Paging的方法。事务对文件进行修改时,会获得新的Page,并加入Current的Page Table,所有的修改都只发生在Current Directory;事务Commit时,Current指向的Page刷盘,并通过原子的操作将Current的Page Table合并到Shadow Directory中,之后再返回应用Commit成功;事务Abort时只需要简单的丢弃Current指向的Page;如果过程中发生故障,只需要恢复Shadow Directory,相当于对所有未提交事务的回滚操作。
虽然Shadow Paging设计简单直观,但它的一些缺点导致其并没有成为主流,首先,不支持Page内并发,一个Commit操作会导致其Page上所有事务的修改被提交,因此一个Page内只能包含一个事务的修改;其次,不断修改Page的物理位置,导致很难将相关的页维护在一起,破坏局部性;另外,对大事务而言,Commit过程在关键路径上修改Shadow Directory的开销可能很大,同时这个操作还必须保证原子;最后,增加了垃圾回收的负担,包括对失败事务的Current Pages和提交事务的Old Pages的回收。更多介绍,可以看这篇文章:数据库故障恢复机制的前世今生 。
影子页的方式不好,那么是否可以让事务中的数据不落盘,等到事务完成时,再落盘了,这种方式理论上时可以的,但是却又一些不足。不足在于,这种方式会造成很大的内存空间压力,另一方面,大量随机IO也会极大的影响性能。因此,数据库实现并没有采用这种方式。
那么数据库采用了什么解决方案了?那就是Undo Log,Undo Log中记录操作前的数据值。数据库以顺序IO的方式连续写入Undo Log,当事务需要回滚时,就通过Undo Log来回滚事务。InnoDB就是这样做的。每当 InnoDB 引擎对一条记录进行操作(修改、删除、新增)时,要把回滚时需要的信息都记录到 undo log 里,比如:
- 在插入一条记录时,要把这条记录的主键值记下来,这样之后回滚时只需要把这个主键值对应的记录删掉就好了;
- 在删除一条记录时,要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了;
- 在更新一条记录时,要把被更新的列的旧值记下来,这样之后回滚时再把这些列更新为旧值就好了。
- 在发生回滚时,就读取 undo log 里的数据,然后做相反操作。比如当 delete 一条记录时,undo log 中会把记录中的内容都记下来,然后执行回滚操作的时候,就读取 undo log 里的数据,然后进行 insert 操作。
3.1.2、多版本并发控制
数据库面临的另一个常见的问题,就是并发读写的问题。在多事务的情况下,什么策略,可以获得最好的并发效果了。我们可以从悲观机制以及乐观机制两个角度出发,相对于悲观的锁实现,乐观的机制可以在冲突发生较少的情况下获得更好的并发效果。在乐观机制里面,而多版本由于避免读写事务与只读事务的互相阻塞, 在大多数数据库场景下都可以取得很好的并发效果,因此被大多数主流数据库采用。InnoDB实现MVCC用的就是Undo Log。
更多并发控制机制,可以看这篇文章:浅析数据库并发控制机制
3.2、Undo Log日志样子
查询不需要Undo,因此只需要对新增,修改,删除操作进行Undo日志记录。仔细思考下,这三个的区别,新增是没有记录的,因此不存在历史版本,反向操作也简单,直接删除就好。而修改和删除则相对复杂一些,因为会有历史版本,需要支持MVCC。因此InnoDB对于Undo分为两种日志:
1) Insert Undo Log:记录对Insert操作产生的日志。
2)Update Undo Log:记录对delete和update操作产生的Undo Log。
3.2.1、Undo Record of the Insert Type
Insert Undo Log的结构如下图所示,插入语句不用于MVCC,只在事务需要回滚时起作用。因此,我们只需要记录它的主键,然后在回滚时找到它,删除就好了。
其中Undo Number是Undo的一个递增编号,Table ID用来表示是哪张表的修改。下面一组Key Fields的长度不定,因为对应表的主键可能由多个field组成,这里需要记录Record完整的主键信息,回滚的时候可以通过这个信息在索引中定位到对应的Record。除此之外,在Undo Record的头尾还各留了两个字节用户记录其前序和后继Undo Record的位置。
3.2.2、Undo Record of the Update Type
由于MVCC需要保留Record的多个历史版本,当某个Record的历史版本还在被使用时,这个Record是不能被真正的删除的。因此,当需要删除时,其实只是修改对应Record的Delete Mark标记。对应的,如果这时这个Record又重新插入,其实也只是修改一下Delete Mark标记,也就是将这两种情况的delete和insert转变成了update操作。再加上常规的Record修改,因此这里的Update Undo Record会对应三种Type:TRX_UNDO_UPD_EXIST_REC、TRX_UNDO_DEL_MARK_REC和TRX_UNDO_UPD_DEL_REC。他们的存储内容也类似:
除了跟Insert Undo Record相同的头尾信息,以及主键Key Fileds之外,Update Undo Record增加了:
Transaction Id记录了产生这个历史版本事务Id,用作后续MVCC中的版本可见性判断;
2. Rollptr指向的是该记录的上一个版本的位置,包括space number,page number和page内的offset。沿着Rollptr可以找到一个Record的所有历史版本。
- Update Fields中记录的就是当前这个Record版本修改前的值,包括所有被修改的Field的编号,长度和历史值。
3.3、Undo Tablespace 管理
3.3.1、Undo Tablespace发展
3.3.2、文件组织方式 - Undo Tablespace
每个写事务都会持有至少一个Undo Segment,当有大量写事务并发运行时,就需要存在多个Undo Segment。InnoDB中的Undo 文件中准备了大量的Undo Segment的槽位,按照1024一组划分为Rollback Segment。每个Undo Tablespace最多会包含128个Rollback Segment,Undo Tablespace文件中的第三个Page会固定作为这128个Rollback Segment的目录,也就是Rollback Segment Arrary Header,其中最多会有128个指针指向各个Rollback Segment Header所在的Page。Rollback Segment Header是按需分配的,其中包含1024个Slot,每个Slot占四个字节,指向一个Undo Segment的First Page。除此之前还会记录该Rollback Segment中已提交事务的History List,后续的Purge过程会顺序从这里开始回收工作。
可以看出Rollback Segment的个数会直接影响InnoDB支持的最大事务并发数。MySQL 8.0由于支持了最多127个独立的Undo Tablespace,一方面避免了ibdata1的膨胀,方便undo空间回收,另一方面也大大增加了最大的Rollback Segment的个数,增加了可支持的最大并发写事务数。如下图所示:
3.3.3、物理组织格式 - Undo Segment
上面描述了一个Undo Log的结构,一个事务会产生多大的Undo Log本身是不可控的,而最终写入磁盘却是按照固定的块大小为单位的,InnoDB中默认是16KB,那么如何用固定的块大小承载不定长的Undo Log,以实现高效的空间分配、复用,避免空间浪费。
InnoDB的基本思路是让多个较小的Undo Log紧凑存在一个Undo Page中,而对较大的Undo Log则随着不断的写入,按需分配足够多的Undo Page分散承载。下面我们就看看这部分的物理存储方式:
如上所示,是一个Undo Segment的示意图,每个写事务开始写操作之前都需要持有一个Undo Segment,一个Undo Segment中的所有磁盘空间的分配和释放,也就是16KB Page的申请和释放,都是由一个FSP的Segment管理的,这个跟索引中的Leaf Node Segment和Non-Leaf Node Segment的管理方式是一致的,这部分之后会有单独的文章来进行介绍。
Undo Segment会持有至少一个Undo Page,每个Undo Page会在开头38字节到56字节记录Undo Page Header,其中记录Undo Page的类型、最后一条Undo Record的位置,当前Page还空闲部分的开头,也就是下一条Undo Record要写入的位置。Undo Segment中的第一个Undo Page还会在56字节到86字节记录Undo Segment Header,这个就是这个Undo Segment中磁盘空间管理的Handle;其中记录的是这个Undo Segment的状态,比如TRX_UNDO_CACHED、TRX_UNDO_TO_PURGE等;这个Undo Segment中最后一条Undo Record的位置;这个FSP Segment的Header,以及当前分配出来的所有Undo Page的链表。
Undo Page剩余的空间都是用来存放Undo Log的,对于像上图Undo Log 1,Undo Log 2这种较短的Undo Log,为了避免Page内的空间浪费,InnoDB会复用Undo Page来存放多个Undo Log,而对于像Undo Log 3这种比较长的Undo Log可能会分配多个Undo Page来存放。需要注意的是Undo Page的复用只会发生在第一个Page。
3.3.4、整体组织结构
Undo日志在内存中的数据结构,以及在磁盘上的组织结构,整体如下图所示:
3.4、Undo For Rollback
InnoDB中的事务可能会由用户主动触发Rollback;也可能因为遇到死锁异常Rollback;或者发生Crash,重启后对未提交的事务回滚。在Undo层面来看,这些回滚的操作是一致的,基本的过程就是从该事务的Undo Log中,从后向前依次读取Undo Record,并根据其中内容做逆向操作,恢复索引记录。
完成回滚的Undo Log部分,会调用trx_roll_try_truncate进行回收,对不再使用的page调用trx_undo_free_last_page将磁盘空间交还给Undo Segment,这个是写入过程中trx_undo_add_page的逆操作。
3.5、Undo For MVCC
多版本的目的是为了避免写事务和读事务的互相等待,那么每个读事务都需要在不对Record加Lock的情况下, 找到对应的应该看到的历史版本。所谓历史版本就是假设在该只读事务开始的时候对整个DB打一个快照,之后该事务的所有读请求都从这个快照上获取。当然实现上不能真正去为每个事务打一个快照,这个时间空间都太高了。
InnoDB的做法,是在读事务第一次读取的时候获取一份ReadView,并一直持有,其中记录所有当前活跃的写事务ID,由于写事务的ID是自增分配的,通过这个ReadView我们可以知道在这一瞬间,哪些事务已经提交哪些还在运行。
作为存储历史版本的Undo Record,其中记录的trx_id就是做这个可见性判断的,对应的主索引的Record上也有这个值。当一个读事务拿着自己的ReadView访问某个表索引上的记录时,会通过比较Record上的trx_id确定是否是可见的版本,如果不可见就沿着Record或Undo Record中记录的rollptr一路找更老的历史版本。
ReadView 到底是什么东西?
Read View 有四个重要的字段:
m_ids: 这个指的是在创建Read View时,当前数据库中活跃事务的事务ID列表,注意这是一个列表,活跃指的是,启动了,但是还没有提交的事务;
min_trx_id: 指的是创建Read View时,当前数据库中活跃事务中的最小事务ID,也就是m_ids中的最小值;
max_trx_id: 这个并不是m_idx中的最大值,而是创建Read View时当前数据库中应该给下一个事务的ID值,也就是全局事务中最大事务ID值+1;
creator_trx_id: 指的是创建该 Read View的事务的事务ID。
记录与事务ID怎么关联?
InnoDB引擎的数据表,它的聚簇索引记录中都包含两个隐藏列:
trx_id: 当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务ID记录在trx_id隐藏列里;
roll_pointer: 每次对某条聚簇索引进行修改时,都会把旧版本的记录写入到undo日志中,然后这个隐藏列是一个指针,指向每一个旧版本记录,这样就可以通过它找到旧版本记录。
如何通过Read View判断可见不可见?
通过Read View,就可以判断出哪些记录是可见的,具体如下所示:
1)如果记录的trx_id值小于Read View中的min_trx_id值,那么表示这个版本的记录在创建Read View前已经完成了,所以是可见的;
2)如果记录的trx_id值大于等于Read View的max_trx_id值,表示是在创建Read View后的事务生成的,因此不可见;
3)如果记录的trx_id在Read View的min_trx_id和max_trx_id之间,那么需要判断是否在m_ids列表中,如果在列表中,表示事务快照时,此事务活跃,那么不可见;如果不在列表中,表示已经提交,可见;
MVCC工作原理举例
如上图所示,共有四个事务,事务R开始需要查询表t上的id为1的记录,R开始时事务I已经提交,事务J还在运行,事务K还没开始。我们看一下事务R访问的情况:
1)事务R从索引中找到对应的这条Record[1, C],对应的trx_id是K,不可见;
2)沿着Rollptr找到Undo中的前一版本[1, B],对应的trx_id是J,不可见;
3)继续沿着Rollptr找到[1, A],trx_id是I可见,返回结果。
五、BinLog日志
4.1、为什么要Binlog
为什么需要Binlog呢?试想一下这样的场景,如果我们把MySQL的数据同步其他节点中,或者同步到其他存储中,我们怎么做?此外,如果我们想对数据库做一个备份,我们怎么做了?这就是Binlog的作用。
4.2、Binlog长什么样子
我们知道InnoDB有Redo Log,是引擎层面的物理日志。Binlog作为逻辑日志,不记录PAGE的修改,只记录数据的变化即可。那么怎么做了?
其中一种方式,就是把每个用户提交的SQL都记录下来,然后在同步或者备份时,重新执行一遍就好了。比如:
update t_user set age=18 where name='小易';
我们按照执行时间,记录下相应的SQL,然后在需要同步的从库上也执行一下这个更新即可。一般的场景是没什么问题,但是遇到一些特殊的场景,比如SQ中包含函数,比如uuid()函数,我们重放这个SQL,会生成不同的uuid,这样就会数据不一致,这显然有问题。那么怎么办了?
我们可以不简单的记录执行的SQL,而是直接改变的数据,比如上面我们SQL中uuid生成了一id,然后将相应的数据插入,那么我们实际记录插入的数据即可,这样就不会出现数据不一致的情况了。
那么直接记录数据变化的方式,是不是完美的了?其实也不是,在更新的场景下,一条Update语句,可能影响很多数据,我们直接记录变化的数据,会浪费存储,以及占用IO,影响性能。
那么怎么办了?可以这样嘛,能直接记录用户SQL的,就直接记录,然后重放就好,不能记录的,就记录数据的变更,这样就没问题了。
上面讨论的,其实就是MySQL Binlog支持的三种格式:Statement, Row,Mixed, 具体为:
- Statement: 记录原始的SQL(查询SQL不用记)到Binlog中;
- Row: 记录被修改的数据;
- Mixed: Statement和 Row的混合体。
4.3、Binlog写入
这里不得不提一下,Binlog与Redo log采用的两阶段提交的方式写入。首先看看为什么要这样。
4.3.1、为什么需要两阶段提交
主要还是一致性考虑,两阶段提交是分布式系统中的常见解决思路之一。试想下,如果不使用两阶段提交会出现什么情况:
1)如果在将 redo log 刷入到磁盘之后, MySQL 突然宕机了,而 binlog 还没有来得及写入。那么从库就同步不到,那么主从不一致;
2)如果在将 binlog 刷入到磁盘之后, MySQL 突然宕机了,而 redo log 还没有来得及写入。binlog有了,那么就会被从库同步过去,而Redo 没有,那么主库数据就会比从库少,仍然不一致。
因此,对于开启了Binlog的情况,需要使用两阶段提交,才能保证出从数据一致。
4.3.2、两阶段提交具体的过程
事务的提交过程有两个阶段,就是将 redo log 的写入拆成了两个步骤:prepare 和 commit,中间再穿插写入binlog,具体如下:
- prepare 阶段:将 XID(内部 XA 事务的 ID) 写入到 redo log,同时将 redo log 对应的事务状态设置为 prepare,然后将 redo log 持久化到磁盘(innodb_flush_log_at_trx_commit = 1 的作用);
- commit 阶段:把 XID 写入到 binlog,然后将 binlog 持久化到磁盘(sync_binlog = 1 的作用),接着调用引擎的提交事务接口,将 redo log 状态设置为 commit,此时该状态并不需要持久化到磁盘,只需要 write 到文件系统的 page cache 中就够了,因为只要 binlog 写磁盘成功,就算 redo log 的状态还是 prepare 也没有关系,一样会被认为事务已经执行成功;
4.3、Binlog 同步
Binlog常用的场景之一,就是主从同步,MySQL从库通过Binlog同步主库的数据,这样可以做读写分离。那么同步的具体流程是怎么样的了?同步的流程是这样的:
1:主库把数据变更写入binlog文件;
2:从库I/O线程发起dump请求;
3:主库I/O线程推送binlog至从库;
4:从库I/O线程写入本地的relay log文件(与binlog格式一样);
5:从库SQL线程读取relay log并重新串行执行一遍,得到与主库相同的数据。
聪明的你,会发现,上述这个是异步复制的模式,它会存在不一致以及数据丢失的问题?其实在分布式架构中,这种问题很多,比如Kafka,怎么保障kafka数据不丢了?我们重点看Producer,Producer发送消息,需要ISR都确认收到才算成功。类比到MySQL这里,怎么保障不丢了,我们可以这样,当所有从库都同步完成了,才算成功,这样就不会有问题了,这就是全同步复制。
全同步复制过于严苛了,将严重影响写入性能,我们可以退一步,只要至少一个同步完成就好,这样不需要等待所有从库完成,性能大大提高,这就是半同步复制。
总结一下同步方式,主要有三种:
- 同步复制:MySQL 主库提交事务的线程要等待所有从库的复制成功响应,才返回客户端结果。这种方式在实际项目中,基本上没法用,原因有两个:一是性能很差,因为要复制到所有节点才返回响应;二是可用性也很差,主库和所有从库任何一个数据库出问题,都会影响业务。
- 异步复制(默认模型):MySQL 主库提交事务的线程并不会等待 binlog 同步到各从库,就返回客户端结果。这种模式一旦主库宕机,数据就会发生丢失。
- 半同步复制:MySQL 5.7 版本之后增加的一种复制方式,介于两者之间,事务线程不用等待所有的从库复制成功响应,只要一部分复制成功响应回来就行,比如一主二从的集群,只要数据成功复制到任意一个从库上,主库的事务线程就可以返回给客户端。这种半同步复制的方式,兼顾了异步复制和同步复制的优点,即使出现主库宕机,至少还有一个从库有最新的数据,不存在数据丢失的风险。
主从延迟处理?
因为数据要从一个机器同步到其他机器,免不了会发生延迟,那么什么情况下会发生主从延迟,我们又怎么去解决呢?
场景一:从库压力大。一般主从模式,读写分离,主库写,从库读,如果从库查询量特别大,那么会耗费大量资源,影响同步进度,造成同步延迟。对于这种情况,可以一主多从,分担读的压力。
**场景二:大事务操作。**主从执行一个大事务,产生了大量的Binlog,那么这些Binlog传输到从库,势必会有一定延迟。对于这种情况,尽量避免大事务操作,比如删除数据,控制数量,分批操作。
**场景三:大查询+DDL。**了解DDL操作的,知道这玩意儿很危险,即便数据库已经有各种优化了,比如Inplace, instant等,但是还是危险。我们在主库DDL操作时,如果遇到大事务,那么可能阻塞主库写,在从库中,如果遇到大查询,也有可能会阻塞从库,此外也可能影响从库DDL操作延迟,因为一致拿不到MDL的写锁。所以,我们要避免大事务和慢查询。
六、对比分析
是否可以用BinLog代替Redo Log?
不可以,我理解原因有:
1)Binlog没有checkpoint,不知道从哪里开始恢复;
2)Binlog是逻辑日志,效率低;
是否可以用Redo Log代替BinLog
不可以,主要原因:
1)Redo Log循环写,并不是全量保存;Binlog追加写,理论上无限大;
2)Redo Log是物理日志,Binlog是逻辑日志,物理日志没法在其他存储上重放。