总会有面试官问:你知道MySQL是如何保证数据不丢失的吗?其实这个问题很不准确。MySQL有太多的方法来保证数据不丢失。但通常面试官想听的是redolog的两阶段提交是如何保证数据不丢失的。(不过个人觉得这样说并不准确)所谓“redolog”就是“重做日志”,就是用来恢复数据的日志。所谓“两阶段提交”也叫“两阶段提交”(Two-PhaseCommit,简称2PC)。本文主要讲MySQL内部XA事务中redolog的二阶段提交的细节。为了让大家吃饱,我先给大家点上亿的开胃菜。虽然有点多,但相信会很开胃。开胃菜你知道什么是存储引擎、随机IO和顺序IO吗?你知道MySQL中的缓冲池吗?你听说过binlog和redolog吗?什么用途?什么?你还不知道?采访结束了。什么是存储引擎?存储引擎是MySQL中直接与磁盘交互的部分。页是存储引擎读写数据的最小单位,一页可以包含一条或多条表记录。MySQL中有很多存储引擎,如InnoDB、MyISAM、Memory等,其中最常用的是InnoDB。InnoDB是MySQL中唯一可以完全支持事务特性的存储引擎,也是一个高性能的存储引擎。本文要讨论的“两阶段提交”发生在InnoDB中。什么是随机IO和顺序IO?有两种方法可以将数据读取和写入磁盘。随机IO需要先找到地址,再读写数据。每次获取的地址都是随机的。就像送外卖一样,每笔订单都送到不同的地址,东奔西跑效率极低。对于顺序IO,由于地址是连贯的,找到地址后,可以一次读写很多数据,效率更高。就像送外卖一样,所有的订单都在同一栋楼里,一次可以送很多,效率很高。什么是缓冲池?关系数据库的特点是需要访问磁盘中的大量数据,因此有时也称为基于磁盘的数据库。正是因为数据库需要经常对磁盘进行IO操作,为了改善直接读写磁盘带来的IO性能问题,引入了缓冲池。缓冲池是一个内存区域。存储引擎读取数据时,会先将页读入缓冲池。下次读取时,先判断是否在缓冲池中,如果在,直接读取,否则从磁盘中读取。修改数据时,如果缓冲池中不存在需要的数据页,则从磁盘中读取到缓冲池中,否则直接修改缓冲池中的数据页。这样做的好处是,如果我们频繁修改位于磁盘上的某个数据页,就不用每次都去磁盘读写(注意是读写)该页,而是直接修改缓冲池中的内容。时间到了,数据被刷新到磁盘。这将使磁盘上的多个操作合二为一。即使修改的内容在磁盘中相距较远的不同数据页上,我们也可以将多个IO到磁盘合并为一个随机IO。修改后的数据页会暂时与磁盘上的数据不一致。我们此时称缓冲池中的数据页为脏页,将页刷入磁盘的操作称为flushingdirtypages(这句话是重点,后面吃)。我们来看看刷脏页的时机:[^1]innodb_max_dirty_pages_pct由于刷脏页的过程仍然是异步的,更新操作不需要等待磁盘IO操作。因此,这些特性大大提高了InnoDB的性能。什么是二进制日志?binlog是在MySQL服务器级别实现的二进制日志,用于记录对数据库的所有更改(这种日志称为逻辑日志)。比如你更新一条记录,服务器会记录一条对应的消息到binlog。但是在InnoDB中,这个binlog是以事务为单位刷新到磁盘的[^2]。基于binlog的特点,我们一般在以下几个方面使用binlog:[^2]数据库的增量备份与恢复:使用backup恢复数据后,可以使用binlog记录的内容来更新备份时间点(参考作为备份点)进行数据恢复。因为binlog也会记录变更操作的时间,所以binlog可以将数据恢复到特定的时间点。这就给我们提供了除了删库跑路之外的第二个选择:使用binlog来恢复数据。主从复制:MySQL从服务器可以通过订阅binlog实现到主服务器的增量复制。审计:通过审计binlog中的数据,判断是否存在安全问题,比如SQL注入。使用binlog的恢复过程是:[^5]首先通过最新的备份恢复数据库的数据,记录备份文件备份的时间点。在binlog中找到这个时间点,提取这个时间点之后的数据来恢复备份点之后的数据(这个特性叫做PointinTime,简称PIT)。各个部分之间的关??系。晚餐开始打开胃口,我们可以吃剩下的内容了。什么是重做日志?前面我们提到,如果数据页在缓冲池中被修改,就会变成脏页。如果此时出现宕机,脏页就会失效,从而导致我们修改的数据丢失,无法保证事务的持久性。保证数据不丢失是重做日志的一个重要功能。我们已经了解到,如果我们修改缓冲池中的数据页并立即刷新脏页,会产生大量的随机IO,导致磁盘性能不佳;但是如果先写buffer,过段时间flushdirtypages,就有可能造成数据丢失,事务的持久性得不到保证。这有点困难。于是救星来了,救星的名字叫WAL(Write-AheadLogging,先记录日志)。即:事务提交前写入日志,然后修改页面(修改页面的时机就是刷脏页的时机)。这里所谓的日志就是重做日志。redolog不会记录整个page的修改,大致是这样的:xx表空间,xx页,xx位置,xx值记录了磁盘中某页某位置数据的修改结果(这个种日志称为物理日志),这样可以节省大量的磁盘空间。因为redolog是顺序写入的(sequentialIO),可以有效的提高IO效率;又因为重做日志是在每次事务提交之前写入的,所以可以保证更新后的数据不会丢失。我们知道,脏页一旦被刷新,磁盘上对应的redolog就会失效,所以redolog用完后,可以再次使用,这样就节省了空间。直到需要刷新redologbuffer,发现下一个redolog对应的脏页还没有刷新,此时会强制刷新脏页。我们已经提到了bufferpool的好处,所以redolog也有类似的redologbuffer。写入redolog时,会先写入redologbuffer,在以下时机将redolog刷入磁盘:[^3]每秒刷新一次。当事务提交时redologbuffer的剩余空间小于1/2,我们应该认为如果dirtypage没有刷新,数据库宕机,那么就需要使用redolog来恢复数据.那么redolog应该从哪里开始恢复数据呢?为了解决这个问题,InnoDB为redolog记录了序号,称为LSN(LogSequenceNumber),可以理解为一个偏移量。日志越新,LSN越大。InnoDB使用一个检查点(checkpoint_lsn)来指示尚未被刷新的数据从这里开始,并使用lsn指示下一个应该写入日志的位置。但是由于有redologbuffer,实际写入磁盘的位置往往比lsn小。为了让大家有个更全面的概念,再吃一道小菜:undolog。InnoDB能够保证对事务的完整支持,主要得益于redolog和undolog。我们说过,重做日志可以保证缓冲池中修改的数据页不丢失,并在数据库宕机后自动恢复丢失的数据。undolog用于实现MVCC和事务回滚。在事务执行过程中,不仅会记录redolog,还会记录undolog。至于更多的细节,你可以自己去了解。那么重做日志是如何保证数据不丢失的呢?如何保证数据不丢失?假设我们有一个包含以下数据的表t1:mysql>select*fromt1;+----+------+|编号|名字|+----+------+|1|a|+----+------+当我们执行如下更新语句时:mysqlbegin;更新t1setname='aa'whereid=1;犯罪;InnoDB的内部流程是这样的:服务端收到启动事务的指令,为该事务生成一个全局唯一的事务id。这个事务id会在记录binlog和redolog的时候用到。如果缓存池中id=1的数据页没有数据,则从磁盘中找到对应的数据页(注意这是数据页,不是记录),将数据页加载到缓存中。修改缓存数据页中id=1的数据。记录数据到redologbuffer[^4],binlogcache[^2]。根据redologflushing策略,redologbuffer在这个过程中可能会被flush到磁盘。服务器收到提交事务的指令。将重做日志缓冲区刷新到磁盘并将事务状态标记为准备。此操作称为重做日志准备。将binlog缓存刷新到磁盘。将重做日志缓冲区刷新到磁盘并将事务的状态标记为提交。此操作称为重做日志提交。将事务执行的结果返回给客户端。这样redolog先prepare,再refreshbinlog,再redologcommit的过程就是一个二阶段提交。这种只保证MySQL内部组件之间数据一致性的操作,也称为内部XA事务;相应地,保证跨服务器数据一致性的两阶段提交称为外部XA事务,即分布式事务。注:XA事务是分布式事务中两阶段提交事务的一种实现。宕机后,当MySQL重启时,InnoDB会自动恢复redolog中checkpoint_lsn后commit状态的事务。如果事务在redolog中的状态为prepare,则需要先检查binlog中是否存在该事务。如果是则恢复,否则回滚(通过undolog回滚。脏页正在刷新更新,但是事务没有提交就崩溃了,所以需要回滚)。摘要发生宕机怎么办?MySQL停机可能发生在整个过程中的任何时候。以刚才的流程为例,假设宕机发生在第5步之后,第6步之前,此时服务器还没有将事务的结果返回给客户端,事务的重做日志可能也可能是不会记录在重做日志中。但是只要事务没有被标记为prepare,我们就认为事务还没有执行,否则redolog用来恢复事务的数据可能是不完整的。因此,只要我们此时选择丢弃未准备好的重做日志,就不会造成任何数据一致性问题。那么接下来的步骤宕机怎么办?这就涉及到为什么需要两阶段提交。为什么我们必须分两个阶段提交?在讲解之前,我们还是要明确两个问题:有binlog为什么还要redolog?有redolog为什么还要用binlog呢?有binlog为什么还要redolog?Binlog不知道数据库在什么时刻丢失了哪部分数据。只能通过从备份点重放binlog记录来恢复数据,比较耗时。Binlog恢复需要手动进行,而redolog可以在服务器重启后自动恢复数据。WAL+write-firstbuffer+异步刷脏页,有效提升磁盘IO效率。有redolog为什么还要用binlog呢?Binlog是server级别的功能,redolog是innoDB的功能。Redolog帮助InnoDB实现性能提升和自动恢复。但是其他存储引擎无法使用重做日志的能力。我们也可以关闭binlog,但是大多数情况下我们会开启它,因为开启它的好处更多。比如主从模式需要订阅binlog进行主从复制,可以通过binlog进行数据库的增量备份和恢复。重做日志有很多好处,我们不能放弃;binlog也有很多好处,我们也不能放弃。换句话说,我们需要启用这两个功能。既然都开启了,我们就要保证redolog和binlog数据的一致性。如果binlog有redolog,没有redolog,那么redolog自动恢复时数据会丢失;否则,redolog有,binlog没有。如果开启主从模式,主服务器恢复数据是因为有redolog,但是从服务器依赖消费的binlog保证和主服务器的数据一致,导致从服务器数据少服务器比主服务器。那么为什么要写两次,难道只能写一次redolog吗?这仍然有不一致之处。比如先写binlog,再写redolog:如果此时并发量很大,我们的binlog会往上写,redolog还没写完,机器宕机了,就会有两个数据之间会有很多不一致。另外,因为binlog的数据是最完整的,这会导致我们从binlog回滚,还得手动回滚。InnoDB最初是一个自恢复存储引擎。这样一来,自恢复特性没了,重做日志不是白开发了吗?使用binlog来恢复redolog就更没必要了,因为binlog不知道从哪里开始恢复(它没有checkpoint_lsn)。另外先写redolog再写binlog:不一致的问题和上面类似。另外,恢复redolog时,每次都需要去binlog中查看事务是否已经写入,严重影响性能。但是如果是两阶段提交,commit阶段的事务会直接恢复,只需要在prepare阶段检查binlog。然后用redolog恢复binlog?首先binlog是server的特性,redolog是InnoDB的特性。两者不是一个级别的。很难说这是否可以做到。第二,即使有可能,也会增加很多复杂性。redolog(物理日志)中记录的数据能否恢复SQL语句,如何恢复都是需要考虑的问题。远不如直接使用两阶段提交方便。两阶段提交会影响性能吗?InnoDB使用组提交方法来最小化两阶段提交对性能的影响。在并发事务较多的情况下,MySQL会将多个事务的重做日志一起提交,大大节省了磁盘IO。细节这里不展开。刷binlog时也会采用类似的策略。晚饭后我们吃甜点吧。如果你理解了上面的内容,你会发现“基于事务消息的分布式事务”使用的是典型的2PC思想,你会发现“基于本地消息的分布式事务”使用的是典型的WAL思想。不懂就赶紧学起来吧!
