MySQL原子性与持久性的保证(undo log, redo log与binlog)

MySQL原子性与持久性的保证(undo log, redo log与binlog)

MySQL的ACID特性

  • 原子性(Atomicity):原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做对于银行转账来所就是要么都做,要么都不做。
  • 一致性(Consistency):一致性是指事务执行前后,数据处于一种合法的状态,这种状态是语义上的而不是语法上的。
  • 隔离性(Isolation):隔离性是指多个事务并发执行的时候,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
  • 持久性(Durability):持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

下面我们着重介绍MySQL原子性与持久性

MySQL原子性的保证–undo log(更具体的在MVCC中)

undo log的基本概念

undo log有两个作用,提供回滚和多个行版本控制(MVCC)。
在数据修改的时候,不仅记录了redo log,还记录了对应的undo,如果因为某些原因事务失败而回滚,可以借助该undo进行回滚。
undo log和redo log记录物理日志不一样,他是逻辑日志。可以认为当delete一条记录是,undo log中记录一条对应的insert记录,反之亦然,当update一条记录时,他记录一条对应相反的update记录。
当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚,有时候应用带行版本控制的时候,也是用过undo log来实现:当读取带某一行的其他事务锁定时,它可以从undo log中分析出改行记录以前的数据是什么,从而提供该行版本信息,让用户实现非锁定一致性读取。
undo log采用段的方式来记录的,每个undo操作在记录的时候占用一个undo log segment。
另外,undo log也会产生redo log,因为undo log也要实现持久性的保护。

undo log的存储方式

innoDB存储引擎对undo的管理采用段的方式。rollback segment称为回滚段,每个回滚段中有1024个undo log segment。
MySQL 5.5之后可以支持128个rollback segment,即支持128*1024个undo操作,还可以通过变量innodb_undo_logs自定义多少个rollback segment,默认值为128。
undo log默认存放在共享表空间中。即保存数据的ibdata1中,如果开启了innodb_file_per_table,将放在每个表的.ibd文件中。
在MySQL5.6中,undo的存放位置还可以通过变量innodb_undo_directory来自定义存放目录,默认值为"."表示datadir。
默认rollback segment全部写在一个文件中,但可以通过设置变量innodb_undo_tablespaces平均分配到多个文件中。

MySQL持久性的保证–redo Log与binlog

InnoDB重要的日志模块:redo log

在《孔乙己》这篇文章中酒店掌柜有一个分班,专门用来记录客人的赊账记录,如果赊账的人不多,那么它可以把顾客和账目写在板上,但是如果赊账的人多了,分班总会有记不下的时候,这个时候掌柜一定还有一个专门记录赊账的账本。如果有人要赊账或者还账的话,掌柜一般有两种做法:

  • 直接把账本翻出来,把这次赊的帐加上去或者扣除掉;
  • 现在粉板上记下这次的帐,等打烊以后再把账本翻出核算。

第一种做法对应MySQL中的做法就是每一次更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后在更新,整个过程IO成本,查找成本都很高
第二种粉板与账本配合的整个过程,就是MySQL里面经常说到的WAL技术,WAL全称为Write-Ahead Logging,他的关键点就是先写日志,再写磁盘,也就是先写粉板,等不忙的时候再写账本。
具体来说,当有一条记录需要更新的时候,InnoDB引擎就会先把记录写道redo log里面,并更新内存,这个时候更新就算完成了。同时InnoDB引擎会在适当的时候,将这个操作记录在磁盘里面,而这个更新往往是在系统比较空闲的时候做的,这就像打烊以后掌柜做的事。
如果今天赊账的不多,掌柜可以等打烊以后在整理,但是如果某天赊账的特别多,粉板写满了,又怎么办呢,这个时候掌柜只好放下手中的活,把粉板中的一部分赊账记录更新到账本中,然后把这些记录从粉板上擦掉,为记录新账腾出空间。
与此类似,InnoDB的redo log是固定大小的,比如可以配置一组4个文件,每个文件的大小为1GB,那么这块粉板总共就可以记录4GB的操作,从头开始写,写到末尾就又回到开头循环写。

在这里插入图片描述

write pos是当前记录的位置,一边写一边后移,写到3号文件末尾后就会到0号文件开头,checkpoint就是当前要擦除的位置,也是往后推移并且循环的,查出记录前要把记录更新到数据文件。
write pos和checkpoint之间的数据是粉板上还空则的部分,可以用来记录新的操作,如果write pos追上checkpoint,表示粉板满了,这时候不能在执行新的更新,得停下来先删除掉一些记录。
有了redo log,InnoDB就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力成为crash-safe。

重要的日志模块:binlog

前面我们讲过,MySQL整体来看,其实就有两块,一块是Server层,负责MySQL功能层面上的事情,还有一块是引擎层,负责存储相关的具体事宜,上面我们聊到的粉板redo log是InnoDB特有的日志,而Server层也有自己的日志,成为binlog(归档日志)。
那么我想问,为什么又两份日志呢。
因为最开始MySQL没有InnoDB引擎,MySQL自带的引擎是MyISAM,但是MyISAM没有crash-safe的能力,binlog日志只能用于归档,InnoDB如果只依靠binlog是没有crash-safe能力的,所以InnoDB使用另外一套日志系统,redo log来实现crash-safe能力。
这两种日志有以下三点不同:

  1. redo log是InnoDB特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用。
  2. redo log是物理日志,记录的是在某个数据页上做了什么修改;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如 “给ID=2这一行的c字段加1”.
  3. redo log是循环写的,空间固定会用完;binlog是可以追加写入的,在文件写道一定大小后会切换到下一个,不会覆盖以前的日志。

我们来看看InnoDB引擎在执行下列MySQL语句的流程。

update T set c=c+1 where ID=2;

在这里插入图片描述

  1. 执行器先找到引擎取ID=2这一行。ID是主键,引擎直接用树搜索找到这一行,如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
  2. 执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在是N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
  3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到redo log里面,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。
  4. 执行器生成这个操作的binlog,并把binlog写入磁盘。
  5. 执行器调用引擎提交事务接口,引擎把刚刚写入的redo log改成提交状态,更新完成。

我们看到,最后三部被拆成了两个步骤:prepare和commit,这就是两阶段提交。

两阶段提交

两阶段提交是为了让两份日志之间的逻辑一致,binlog会记录所有的逻辑操作,并且采用追加写的形式。同时系统会定期做整库备份。这里的定期取决于系统的重要性,可以是一天一备,也可以是一周一备。
如果需要恢复到指定的某一秒,比如某天下午两点发现中午十二点有一次误删表,需要找回数据,那么我们可以这么做:

  • 首先,找到最近的一次全量备份,如果你运气好,该备份那个就是昨天晚上的一个备份,从这个备份恢复到数据库。
  • 从备份的时间点开始,将备份的binlog依次取出来,重放如中午误删表之前的哪个时刻。

为什么我们需要两阶段提交?

由于redolog和binlog是两个独立的逻辑,如果不用两阶段提交,要么就是先写完redo log,再写binlog,或者采用反过来的顺序,来看下会有什么问题。
仍然用前面的update语句来做例子,假设当前ID=2的行,字段c的值是0,在假设执行update语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了crash,那么分为两种情况。

  1. 先写redo log后写binlog。假设在redo log写完,binlog没有写完的时候,MySQL进程异常重启。由于我们前面说过的,redo log写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行c的值是1.由于binlog没有写完就crash了,这时候binlog里面就没有记录这个语句,因此之后备份日志的时候,存起来的binlog里面就没有这条语句。如果需要使用这个binlog来恢复临时库的话,由于这个语句的binlog丢失,这个临时库就会少了这一次的更新,回复出来的这一行c的值就是0,与原库不同。
  2. 先写binlog后写redo log。如果binlog写完之后crash,由于redo log还没写,崩溃恢复以后这个事务无效,所以这一行的c的值是0.但是binlog里面已经记录了把c从0修改为1这个日志,所以之后再用binlog来恢复的时候就多了一个事务出来,恢复出来的这一行的值就是1,与原库的值不同。

在两阶段提交的不同时刻MySQL出现异常,重启后会出现什么情况

我们假设在redo log处于prepare阶段,写binlog之前为时刻A,在写binlog之后,redo log处于commit阶段之前为时刻B
如果在时刻A发生crash,由于binlog还没有写,redo log也还没有提交,所以崩溃恢复的时候,这个事务会回滚。这时候,binlog还没写,所以也不会传到备库中,binlog和redo log都没有写入,相当于没有执行这条命令。所以MySQL重启后和将来使用binlog进行数据恢复的数据库的状态是一样的。
如果在时刻B发生crash,这时候binlog写完,redo log还没有commit,那么在崩溃恢复时MySQL会做以下判断规则:

  1. 如果redo log里面的事务是完整的,也就是已经有了commit标识,则直接提交;
  2. 如果redo log里面的事务只有完整的prepare,则判断对应的事务binlog是否存在并完整:
    a. 如果是,则提交事务。
    b. 否则,回顾事务。
MySQL怎么知道binlog是完整的?

一个事务的binlog是有完整格式的:

  • statement格式的binlog,最后会有COMMIT;
  • row格式的binlog,最后会有一个XID event。

另外,在MySQL 5.6.2版本以后,还引入了binlog-checksum参数,用来验证binlog内容和正确性,对于binlog日志由于磁盘的原因,可能会在日志中间出错的情况,MySQL可以通过校验checksum的结果来发现,所以,MySQL还是有办法验证事务binlog的完整性的。

redo log和binlog是怎么关联起来的?

他们都有一个共同的数据字段,叫XID。奔溃恢复的时候,会被顺序扫描redo log:

  • 如果碰到既有prepare,又有commit的redo log,就直接提交。
  • 如果碰到只有prepare,没有commit的redo log,就拿着XID去binlog找对应的事情。
处于prepare阶段的redolog加上完整的binlog,重启就能恢复,MySQL为什么要这么设计?

因为binlog一旦写入完成之后,那么这个binlog是完整的,如果这个时候MySQL发生崩溃,在重新启动之后,该binlog会被从库使用,所以主库也要提交这个事务,采用这个策略,主库和备库的数据就保证了一致性。

如果这样,为什么还要两阶段提交?为什么要先redolog写完,在写binlog,崩溃恢复的时候,必须两个日志完整才可以,是不是一样的逻辑?

对于InnoDB引擎来说,如果redo log提交完成了,事务就不能回滚,如果允许回滚,就有可能覆盖掉别的事务。而如果redo log直接提交,然后binlog写入的时候失败,InnoDB有回滚不了,数据和binlog日志有不一致了,两阶段提交就是为了给所有人一个机会,等待所有人OK。

不引入两个日志,也就没有两阶段提交的必要了。只用binlog来支持崩溃恢复,又能支持归档,不就可以了?

在这里插入图片描述

binlog没有能力恢复"数据页"
在图中所标注的位置,也就是binlog2写完了,但是整个事务还没有commit的时候,MySQL发生了crash。重启后,引擎内部事务2会回滚,然后应用binlog2可以补回来,但是,对于事务1,来说,系统认为已经提交完成,不会再应用一次binlog1。
InnoDB引擎使用的是WAL技术,执行事务的时候,写完内存和日志,事务就算完成了。如果之后崩溃,要依赖与日志来恢复数据页。
再图中这个位置如果发生崩溃,事务1也是可能丢失了的,而且是数据页级的丢失,此时,binlog里面并没有记录数据页的更新细节,是补不回来的。

能不能只用redo log,不用binlog?

如果只从崩溃恢复的角度来说,是可以的,但是因为binlog拥有归档和赋值功能,如果关掉binlog,很多系统都无法使用,总的来说还是生态不行。

redo log一般设置多大?

如果redo log太小,会导致文件很快就被写满,然后不得不强行刷redo log,很容易就会使MySQL抖,这样WAL机制的能力就发挥不出来了,如果磁盘足够大,那么可以设置为4个1GB。

正常运行中的实例,数据写入后的最终落盘,是从redo log更新过来的还是从buffer pool更新过来的?

redo log并没有记录数据页的完整数据,所以他并没有能力自己去更新磁盘数据页,也就不存在"数据最终落盘,是由redo log更新过去" 的情况。

  1. 如果正常运行的实例,数据页被修改后,跟磁盘的数据页不一致,成为脏页,最终数据落盘,就是把内存中的数据页写盘,这个过程,甚至与redo log毫无关系。
  2. 再崩溃恢复场景中,InnoDB如果判断一个数据页可能再崩溃恢复的时候丢失了更新,就会把他读到内存中,然后让redo log更新内存内容,更新之后,内存页变为脏页,就回到了第一种情况的状态。
redo log buffer 是什么? 实现修改内存,还是先写redo log文件?

再一个事务更新过程中,日志是要写多次的。例如

begin;
insert into t1 ...
insert into t2 ...
commit;

这个事务要往两个表中插入记录,插入数据的过程中,生成的日志都先保存起来,但又不能再还没commit的时候就直接写道redo log文件里。
所以redo log buffer 就是一块内存,用来保存redo日志的,也就十所,再执行第一个insert的时候,数据的内存被修改了,redo log buffer 也写入了日志。
但是,真正把日志写道redo log文件,实在执行commit语句的时候做的,单独执行一个更新语句的时候,InnoDB会自己启动一个事务,再语句执行完成的时候提交。过程跟上面一样,只不过是压缩到了一个语句里面完成。

undo与redo如何记录事务

假设有A,B两个数据,值分别为1,2,开始一个事务,事务的操作内容为:把1修改为3,2修改为4,那么实际的记录如下:

  1. 事务开始。
  2. 记录A=1到undo log。
  3. 修改A=3。
  4. 记录A=3到redo log。
  5. 记录b=2到undo log。
  6. 修改B=4。
  7. 记录B=4到redo log。
  8. 将redo log写入磁盘。
  9. 事务提交。

上述记录中,2,4,5,7,8均为新增操作,但是2,4,5,7为缓冲到buffer区,只有8增加了IO操作,为了保证redo log能够有比较好的IO性能,InnoDB的redo log的设计有以下几个特点:

  1. 尽量保持redo log存储在一段连续的空间上。因此在系统第一次启动时就会将日志文件的空间完全分配。以顺序追加的方式记录Redo Log,通过顺序IO来改善性能。
  2. 批量写入日志。日志并不是直接写入文件,而是先写入redo log buffer.当需要将日志刷新到磁盘时 (如事务提交),将许多日志一起写入磁盘.
  3. 并发的事务共享Redo Log的存储空间,它们的Redo Log按语句的执行顺序,依次交替的记录在一起,以减少日志占用的空间。
  4. 因为3的原因,当一个事务将redo log写入磁盘时,也会将其他未提交的事务的日志写入磁盘。
  5. redo log上只进行顺序追加的操作,当一个事务需要回滚时,它的redo log记录也不会从redo log中删除掉。

redo log的恢复

由于未提交的事务和回滚了的事务也会记录redo log,因此在进行恢复时,这些事务要进行特殊的处理,有两种不同的恢复策略:

  • 进行回复时,只重做已经提交了的事务。
  • 进行回复时,重做所有事务包括未提交的事务和回滚了的事务,然后通过undo log回滚的哪些未提交的事务。

MySQL数据库InnoDB存储引擎使用了B策略,InnoDB存储引擎中的恢复机制有几个特点:

  1. 在重做redo log时,并不关心事务性,恢复时,没有begin,也没有commit,rollback的行为。也不关心每个日志是哪个事务的。尽管事务ID等事务相关的内容会记入Redo Log,这些内容只是被当作要操作的数据的一部分。
  2. 使用第二个策略就必须将undo log持久化,而且必须要在写redo log之前将对应的undo log写入磁盘,undo和redo的这种关联,使得持久化变得复杂起来,为了降低复杂度,InnoDB将undo看作数据,因此记录undo log的操作也会记录到redo logzhong,这样,undo log就可以像数据一样缓存起来,而不用在redo log之前写入磁盘。包含undo log的redo log,看起来是这样的:
记录1: <trx1, Undo log insert <undo_insert …>>
记录2: <trx1, insert …>
记录3: <trx2, Undo log insert <undo_update …>>
记录4: <trx2, update …>
记录5: <trx3, Undo log insert <undo_delete …>>
记录6: <trx3, delete …>
  1. 既然Redo没有事务性,那岂不是会重新执行被回滚了的事务?确实是这样。同时Innodb也会将事务回滚时的操作也记录到redo log中。回滚操作本质上也是对数据进行修改,因此回滚时对数据的操作也会记录到Redo Log中。
记录1: <trx1, Undo log insert <undo_insert …>>
记录2: <trx1, insert A…>
记录3: <trx1, Undo log insert <undo_update …>>
记录4: <trx1, update B…>
记录5: <trx1, Undo log insert <undo_delete …>>
记录6: <trx1, delete C…>
记录7: <trx1, insert C>
记录8: <trx1, update B to old value>
记录9: <trx1, delete A>
  • 6
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
MySQL中的Redo LogBinlogUndo Log是三种不同的日志类型,用于支持数据库事务的持久性、复制和回滚操作。 1. Redo Log(重做日志): Redo LogMySQL引擎内部使用的一种日志,记录了所有已提交的修改操作,以保证数据库在发生崩溃等异常情况下能够进行恢复。当数据库发生崩溃时,可以通过Redo Log来重放这些修改操作,使数据库恢复到崩溃前的状态。Redo Log是在InnoDB存储引擎中实现的,通常以磁盘文件形式存在,可被视为一种类似于事务日志的机制。 2. Binlog(二进制日志): BinlogMySQL数据库服务器层产生的一种日志,用于记录数据库中所有的修改操作,包括数据修改和数据定义语句(DDL)。与Redo Log不同,Binlog记录的是逻辑操作而不是物理操作,以提供对数据的逻辑复制和恢复能力。Binlog通常以二进制文件的形式存在,并且可以被用于主从复制和数据恢复等任务。 3. Undo Log(回滚日志): Undo Log是用于支持事务回滚操作的一种日志。当一个事务执行修改操作时,旧值会被记录在Undo Log中,以便于回滚操作时能够恢复到之前的状态。Undo Log通常与事务的隔离级别和并发控制有关,主要用于MVCC(多版本并发控制)的实现。 这三种日志在MySQL中扮演了不同角色,分别用于保证数据的持久性、支持复制和提供事务回滚功能。在数据库的正常运行和异常恢复中起到至关重要的作用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值