通过上一个部分的学习我们知道了mysql中一个select语句执行的过程,大致为通过连接器获取连接,然后经过查询缓存,未命中则走分析器、优化器、执行器,最终到达存储引擎。
那么一个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(写前日志记录),它的关键点就是先写日志,再写磁盘。
重做日志
作用:确保事务的持久性。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启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。
归档日志(二进制日志)
作用:用于复制,在主从复制中,从库利用主库上的binlog进行重播,实现主从同步。
用于数据库的基于时间点的还原。
内容:逻辑格式的日志,可以简单认为就是执行过的事务中的sql语句。
但又不完全是sql语句这么简单,而是包括了执行的sql语句(增删改)反向的信息,也就意味着delete对应着delete本身和其反向的insert;update对应着update执行前后的版本的信息;insert对应着delete和insert本身的信息。
形式:binlog 有三种模式:Statement(基于 SQL 语句的复制)、Row(基于行的复制) 以及 Mixed(混合模式)
两种日志有以下三点不同:
有了对这两个日志的概念性理解,我们再来看执行器和 InnoDB 引擎在执行这个简单的 update 语句时的内部流程。
放出 update 语句的执行流程图,图中浅色框表示是在 InnoDB 内部执行的,深色框表示是在执行器中执行的。
上面执行步骤中将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"。他的目的是为了让两份日志之间的逻辑一致。
我们可以使用反证法来解释一下两阶段提交的必要性。由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。我们看看这两种方式会有什么问题。
仍然用前面的 update 语句来做例子。假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?
可以看到,如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致。简单点说,redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。