update语句的执行过程

更多内容关注微信公众号:fullstack888

昨天很多同学在后台问,为什么Update是二阶段提交?紧接上文,我们了解了:深入理解Mysql执行过程及执行顺序。今天我们主要分析一下update语句是怎么执行的呢?


假设现在有一个表,只有两个字段:

  •     一个主键字段 id

  •     一个整型字段c

如果需要我们将id=2这一行的字段c值加1,SQL语句是这样的:

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

可以确定的是,查询语句的那一套流程,更新语句也是会走一遍。

首先连接器构建连接,连接器连接之后,如果是8.0之前的MySQL的话,应该还是会先去匹配一下查询缓存,然后发现没有命中这条查询语句。接着分析器会通过词法和语法解析知道这是一条更新语句。优化器决定要使用 ID 这个索引。然后,执行器负责具体执行,找到这一行,然后更新。再分析更新操作具体是如何实现之前,我们先来了解一个场景:


我们经营了一家饭店。饭店开张,客人会有赊账,还账,直接付账的情况,对应的掌柜的就需要记账,需要查账,修改账本上的赊账金额,或者删除某个赊账金额。对应的就正好是MySQL里面的增删改查操作。


客人不多时,掌柜的可以直接拿着账本去修改。但是客人很多,账本很厚的时候,掌柜的直接去修改账本就会影响做生意。所以聪明的掌柜的除了账本之外,还会有一个粉板,粉板上记录着当天都谁来还账了,谁来赊账,等到饭店打烊的时候,再根据粉板上记录的内容去改账本上的内容。


在 MySQL 里也有这个问题,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高。为了解决这个问题,MySQL 的设计者就用了类似酒店掌柜粉板的思路来提升更新效率。引入了两个日志。redo log(重做日志)binlog(归档日志)这也是我们今天需要 主要了解的主要内容。


上面的这种操作叫做WAL 技术。WAL 的全称是 Write-Ahead Logging(写前日志记录),它的关键点就是先写日志,再写磁盘。


InnoDb引擎日志:redo  log

重做日志


作用:确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。


内容:物理格式的日志,记录的是物理数据页面的修改的信息,其redo log是顺序写入redo log file的物理文件中去的。


结合着我们今天的更新语句来说的话,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log(粉板)里面,并更新内存,这个时候更新就算完成了。同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做,这就像打烊以后掌柜做的事。


如果今天赊账的不多,掌柜可以等打烊后再整理。但如果某天赊账的特别多,粉板写满了,又怎么办呢?这个时候掌柜只好放下手中的活儿,把粉板中的一部分赊账记录更新到账本中,然后把这些记录从粉板上擦掉,为记新账腾出空间。


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


write pos 是当前记录的位置,一边写一边后移。checkpoint 是记录当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。


如图所示只有被擦除过的区域才能继续书写,也就是write pos 和 checkpoint 之间的那部分空着的区域,是可以用来记录新的操作。如果 write pos 追上 checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。


有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。


注意这里仅仅是之前提交的记录不会丢失,我们都知道数据库的操作都涉及到事务,想要保证我们的事务完全正确,操作的记录和数据库数据保持一致,单单一个redo log是不行的,还需要另一个日志binlog。


server层日志:binlog

归档日志(二进制日志)


作用:用于复制,在主从复制中,从库利用主库上的binlog进行重播,实现主从同步。 

           用于数据库的基于时间点的还原。


内容:逻辑格式的日志,可以简单认为就是执行过的事务中的sql语句。


但又不完全是sql语句这么简单,而是包括了执行的sql语句(增删改)反向的信息,也就意味着delete对应着delete本身和其反向的insert;update对应着update执行前后的版本的信息;insert对应着delete和insert本身的信息。


形式:binlog 有三种模式:Statement(基于 SQL 语句的复制)、Row(基于行的复制) 以及 Mixed(混合模式)


两种日志有以下三点不同:

  • redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用。

  • redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”。

  • redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。


两阶段提交

有了对这两个日志的概念性理解,我们再来看执行器和 InnoDB 引擎在执行这个简单的 update 语句时的内部流程。

  • 执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。

  • 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。

  • 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。

  • 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。

  • 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。

放出 update 语句的执行流程图,图中浅色框表示是在 InnoDB 内部执行的,深色框表示是在执行器中执行的。


上面执行步骤中将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"。他的目的是为了让两份日志之间的逻辑一致。


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


仍然用前面的 update 语句来做例子。假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?


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

先写 binlog 后写 redo log。如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0。但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同。

可以看到,如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。简单点说,redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。


小结

redo log 用于保证 crash-safe 能力。innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数我建议你设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。

sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数我也建议你设置成 1,这样可以保证 MySQL 异常重启之后 binlog 不丢失。

两阶段提交是跨系统维持数据逻辑一致性时常用的一个方案,即使不做数据库内核开发,日常开发中也有可能会用到。

- END -

推荐阅读:

深入理解Mysql执行过程及执行顺序

你真正思考过什么是架构吗?

分表分页/跨库分页为什么这么难?

墙裂推荐:灰常好用的画图工具

网易计费系统架构升级之路

NoOps 来了,DevOps 工程师的未来在何方?

关注:fullstack888

学习架构知识

互联网后端架构



本文分享自微信公众号 - 互联网后端架构(fullstack888)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部