InnoDB事务与各种日志的关系

事务的四大特性

能被称为事务,就一定具备以下四个特性:分别是原子性、一致性、隔离性、持久性。俗称ACID。

原子性(Atomicity)

原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。

一致性(Consistency)

一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。举例来说,假设用户A和用户B两者的钱加起来一共是1000,那么不管A和B之间如何转账、转几次账,事务结束后两个用户的钱相加起来应该还得是1000,这就是事务的一致性。

隔离性(Isolation)

隔离性是当多个用户并发访问数据库时,比如同时操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。关于事务的隔离性数据库提供了多种隔离级别,稍后会介绍到。

持久性(Durability)

持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。例如我们在使用JDBC操作数据库时,在提交事务方法后,提示用户事务操作完成,当我们程序执行完成直到看到提示后,就可以认定事务已经正确提交,即使这时候数据库出现了问题,也必须要将我们的事务完全执行完成。否则的话就会造成我们虽然看到提示事务处理完毕,但是数据库因为故障而没有执行事务的重大错误。这是不允许的。

MySQL是如何保证事务原子性与持久性的?

首先,我们先思考一下,MySQL在实现事务时都遇到了哪些难点?

首先,我们知道,MySQL中有Buffer Pool这个概念。也就是说,客户端许多的修改操作,仅仅存入了内存中,还没有真正落地到磁盘时,就通知客户端事务完成了。

假设不幸,MySQL服务器宕机会造成什么后果呢?那就是内存数据丢失,这就有违我们MySQL的持久性这一原则。

那么如何保证每个提交后的事务,都保证最后一定能落地到磁盘中呢?

WAL机制保证持久性

MySQL 采用的是 WAL(Write-ahead logging,预写式日志)机制来实现的。

网上很多解释是这样的:所谓的WAL机制,就是先写日志,再写磁盘。当然这个解释绝对是正确的。只是懂得人一眼就懂,然而不懂的人容易混淆。我们现在站在MySQL全局的角度来分析,一个MySQL事务真正修改了哪些部分。

执行一个事务,MySQL做了哪些事情

我们先拿redo log为例,执行这个插入语句:

insert into t(id,k) values(id1,k1),(id2,k2);

这里,我们假设当前是以k为索引的二级B+树索引,查找到位置后,k1所在的数据页在内存(InnoDB buffer pool)中,k2所在的数据页不在内存中。

image

分析这条更新语句,你会发现它涉及了四个部分:内存、redo log(ib_log_fileX)、 数据表空间(t.ibd)、系统表空间(ibdata1)。

这条更新语句做了如下的操作(按照图中的数字顺序):

  1. Page 1在内存中,直接更新内存;
  2. Page 2没有在内存中,就在内存的change buffer区域,记录下“我要往Page 2插入一行”这个信息
  3. 将上述两个动作记入redo log中(图中3和4)。

做完上面这些,事务就可以完成了。

也就是说,一个更新事务的完成指标有两个:

  1. 修改Buffer Pool中的缓存。
  2. 记录日志。

真正的MySQL数据何时落地到磁盘,是由许多机制共同决定的,但绝对不是一个同步的动作。如果你学习过JVM,你很容易联想到内存数据持久化到磁盘的时机就像JVM的垃圾回收机制一般。在后面内容会和大家一起仔细分析。

因此,以全局的角度,我们将MySQL刷盘流程(包含WAL机制)总结为:先写内存,再写日志,最后写磁盘。

所以,假设事务已经成,但数据在内存的情况下发生了宕机,那么WAL技术就可以保证这些已完成的事务的数据恢复。

重要的日志模块:redo log

redo log的作用

InnoDB 存储引擎是以页为单位来管理存储空间的,我们进行的增删改查操作其实本质上都是以页为单位进行操作的。

在真正访问页面之前,需要把在磁盘上的页缓存到内存中的 Buffer Pool 之后才可以访问。但是在事务的时候又强调过一个称之为持久性的特性,就是说对于一个已经提交的事务,在事务提交后即使系统发生了崩溃, 这个事务对数据库中所做的更改也不能丢失。

如果我们只在内存的 Buffer Pool 中修改了页面,假设在事务提交后突然发生了某个故障,导致内存中的数据都失效了,那么这个已经提交了的事务对数据库中所做的更改也就跟着丢失了,这是我们所不能忍受的。redo log因此而诞生了,配合上之前讲的WAL技术原理,就可以将在内存中尚未刷盘的丢失数据重新找回。

特别说明一下:redo log是InnoDB的产物,别的引擎并没有这个日志。也就是别的引擎是很难解决数据库异常崩溃后的数据恢复问题的。

redo log的底层结构

InnoDB的redo log是固定大小的,比如可以配置为一组4个文件,每个文件的大小是1GB,那么这块“粉板”总共就可以记录4GB的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。

image.png

图中的write pos就是事务在提交之前写入的redo 。check point则是代表着已经成功刷盘的指针位置。那么显然,check point和write pos之间的区域就是未成功刷盘的脏页数据。这两个指针也被持久化保存着。假设发生了宕机,那么MySQL重启后,就可以直接根据这两个指针继续恢复数据。

当然,这个图可能依然不够直观,因此再用一张图帮你加深理解。

image.png

如图,假设把checkpoint位置从CP推进到CP,就需要将两个点之间的日志(浅绿色部分),对应的所有脏页都flush到磁盘上。之后,图中从write pos到CP’之间就是可以再写入的redo log的区域。

redo 日志的写入过程

image.png

为了解决磁盘速度过慢的问题而引入了 Buffer Pool。同理, 写入 redo 日志时也不能直接直接写到磁盘上。实际上在服务器启动时就向操作系统申请了一大片称之为 redo log buffer 的连续内存空间。

当一个事务产生了多条redo log时,并不会产生一条就写一条,而是先暂时缓存到这个redo log buffer中。在事务提交时,可以不把修改过的 Buffer Pool 页面刷新到磁盘。 但是为了保证持久性,必须要把修改这个事务临时缓存在redo log buffer中的数据,刷到真正的redo log日志中。

innodb_flush_log_at_trx_commit 的用法

我们前边说为了保证事务的持久性,用户线程在事务提交时需要将该事务执 行过程中产生的所有 redo 日志都刷新到磁盘上。会很明显的降低数据库性能。 如果对事务的持久性要求不是那么强烈的话,可以选择修改一个称为 innodb_flush_log_at_trx_commit 的系统变量的值,该变量有 3 个可选的值:

0:当该系统变量值为 0 时,表示在事务提交时不立即向磁盘中同步 redo 日 志,这个任务是交给后台线程做的。 这样很明显会加快请求处理速度,但是如果事务提交后服务器挂了,后台线 程没有及时将 redo 日志刷新到磁盘,那么该事务对页面的修改会丢失。

1:当该系统变量值为 1 时,表示在事务提交时需要将 redo 日志同步到磁盘, 可以保证事务的持久性。1 也是 innodb_flush_log_at_trx_commit 的默认值。

2:当该系统变量值为 2 时,表示在事务提交时需要将 redo 日志写到操作系统的缓冲区中(不是redo log buffer),但并不需要保证将日志真正的刷新到磁盘。 这种情况下如果数据库挂了,操作系统没挂的话,事务的持久性还是可以保证的,但是操作系统也挂了的话,那就不能保证持久性了。

简单了解undo日志

因一些原因(机器宕机/操作系统错误/用户主动rollback等)导致事务执行到一半,但这时事务的执行已经让很多信息修改了(提交前就会边执行边修改记录),但还有部分未执行,为了保证事务的一致性与原子性,要么全都执行成功,要么全都失败,所以就需要回滚,而rollback需要旧值依据,而这些旧值记录就存储在undo日志中。

结合redo log也十分好理解。当这个事务没提交之前,无法确保redo log日志是写完的。那这个事务只能通过undo log进行回滚了。

除此之外,我们MVCC实现的可重复读隔离级别,实际上也是由undo log帮忙回滚的。

重要的日志模块:binlog

binlog的作用

前面我们讲过,MySQL整体来看,其实就有两块:一块是Server层,它主要做的是MySQL功能层面的事情;还有一块是引擎层,负责存储相关的具体事宜。上面我们聊到的redo log是InnoDB引擎特有的日志,而Server层也有自己的日志,称为binlog(归档日志)。

Server层的log必然是被所有引擎共享的。binlog看这个名字,就知道也是记录数据用的。那么既然有了binlog,为什么还要redo log呢?他们有啥不一样呢?

因为最开始MySQL里并没有InnoDB引擎。MySQL自带的引擎是MyISAM,但是MyISAM没有crash-safe的能力,binlog日志只能用于归档。而InnoDB是另一个公司以插件形式引入MySQL的,既然只依靠binlog是没有crash-safe能力的,所以InnoDB使用另外一套日志系统——也就是redo log来实现crash-safe能力。

redo log和bin log的区别(面试必问)

  1. 这两者使用方式不一样。binlog 会记录表所有更改操作,包括更新删除数据,更改表结构等等,主要用于人工恢复数据,而 redo log 对于我们是不可见的,它是 InnoDB 用于保证 crash-safe 能力的,也就是在事务提交后 MySQL 崩溃的话,可以保证事务的持久性,即事务提交后其更改是永久性的。 一句话概括:binlog 是用作人工恢复数据,redo log 是 MySQL 自己使用, 用于保证在数据库崩溃时的事务持久性。
  2. redo log 是 InnoDB 引擎特有的,binlog 是 MySQL 的 Server 层实现的, 所有引擎都可以使用。
  3. redo log 是物理日志,记录的是“在某个数据页上做了什么修改”,恢复的速度更快;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这的 c 字段加 1 ” ;
  4. redo log 是“循环写”的日志文件,redo log 只会记录未刷盘的日志,已经刷入磁盘的数据都会从 redo log 这个有限大小的日志文件里删除。binlog 是追加日志,保存的是全量的日志。
  5. 当数据库 crash 后,想要恢复未刷盘但已经写入 redo log 和 binlog 的数据到内存时,redo log记录维护了两个指针write pos(事务写到的位置)和check point(成功刷盘的位置),由这两个位置差,配合上原本磁盘中的数据页,就可以恢复原本在Buffer Pool中尚未刷盘而丢失的数据。binlog 没有类似的记录标志,因此是无法恢复的。

binlog数据恢复流程

如何使用binlog让数据库恢复到半个月内任意一秒的状态?

前面我们说过了,binlog会记录所有的逻辑操作,并且是采用“追加写”的形式。如果你的DBA承诺说半个月内可以恢复,那么备份系统中一定会保存最近半个月的所有binlog,同时系统会定期做整库备份。这里的“定期”取决于系统的重要性,可以是一天一备,也可以是一周一备。

当需要恢复到指定的某一秒时,比如某天下午两点发现中午十二点有一次误删表,需要找回数据,那你可以这么做:

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

这样你的临时库就跟误删之前的线上库一样了,然后你可以把表数据从临时库取出来,按需要恢复到线上库去。(友情提示,删库跑路的时候,记得得把binlog一起删了)。

事务的两阶段提交

什么是两段式提交?

所谓两段式提交,是针对于事务提交前,数据必须完成的写入redo log以及bin log中才算是完成的提交。要保证这两个日志都成功写入,就必须使用两段式提交方式。

两段式提交的流程

现有sql语句如下:

create table T(ID int primary key, c int);
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改成提交(commit)状态,更新完成。

这里我给出这个update语句的执行流程图,图中浅色框表示是在InnoDB内部执行的,深色框表示是在执行器中执行的:

image

你可能注意到了,最后三步看上去有点“绕”,将redo log的写入拆成了两个步骤:prepare和commit,这就是"两阶段提交"。直观来说,就是redo log和bin log变成了串行写,写完才能提交。

没有两段式提交发生的问题

由于redo log和binlog是两个独立的逻辑,如果不用两阶段提交,要么就是先写完redo log再写binlog,或者采用反过来的顺序。我们看看这两种方式会有什么问题。

先写redo log后写binlog。

redo log有值,binlog没值时,MySQL宕机。此时重启MySQL可以通过redo log正常恢复数据。但是如果哪天想还原MySQL,或者用于主从复制时,binlog中少了这部分值的丢失,导致恢复数据时少了一部分数据。

先写binlog后写redo log。

redo log没值,binlog已经成功记录。都知道,redo log没写完,肯定事务不会成功,执行的操作都会根据undo log回滚。但此时binlog中依然保存着这个理应回滚的数据。导致恢复数据时多出一部分数据。

可以看到,如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。

而且不只是误操作后需要用这个过程来恢复数据。当你需要扩容的时候,也就是需要再多搭建一些备库来增加系统的读能力的时候,现在常见的做法也是用全量备份加上应用binlog来实现的,这个“不一致”就会导致你的线上出现主从数据库不一致的情况。

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大将黄猿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值