一条SQL更新语句是如何执行的?

破局之路课程 2024-07-21 20:58:39

查询语句的那一套流程,更新语句也是同样会走一遍具体可参见:

查询流程

更新流程还涉及两个重要的日志模块,redo log(重做日志)和 binlog(归档日志)。

执行流程

执行器和 InnoDB 引擎在执行这个简单的 update 语句时的内部流程。

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

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

可能注意到了,最后三步看上去有点“绕”,将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"。

redo log

WAL 的全称是 Write-Ahead Logging,它的关键点就是先写日志,再写磁盘,也就是先写粉板,等不忙的时候再写账本。

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

write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。

write pos 和 checkpoint 之间的是“粉板”上还空着的部分,可以用来记录新的操作。如果 write pos 追上 checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。

binlog

redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog(归档日志)。

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

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

可以说MySQL数据库的数据备份、主备、主主、主从都离不开binlog,需要依靠binlog来同步数据,保证数据一致性。

记录格式

binlog 日志有三种格式,可以通过binlog_format参数指定。

statementrowmixed

指定statement,记录的内容是SQL语句原文,比如执行一条update T set update_time=now() where id=1,记录的内容如下。

同步数据时,会执行记录的SQL语句,但是有个问题,update_time=now()这里会获取当前系统时间,直接执行会导致与原库的数据不一致。

为了解决这种问题,我们需要指定为row,记录的内容不再是简单的SQL语句了,还包含操作的具体数据,记录内容如下。

row格式记录的内容看不到详细信息,要通过mysqlbinlog工具解析出来。

update_time=now()变成了具体的时间update_time=1627112756247,条件后面的@1、@2、@3 都是该行数据第 1 个~3 个字段的原始值(假设这张表只有 3 个字段)。

这样就能保证同步数据的一致性,通常情况下都是指定为row,这样可以为数据库的恢复与同步带来更好的可靠性。

但是这种格式,需要更大的容量来记录,比较占用空间,恢复与同步时会更消耗IO资源,影响执行速度。

所以就有了一种折中的方案,指定为mixed,记录的内容是前两者的混合。

MySQL会判断这条SQL语句是否可能引起数据不一致,如果是,就用row格式,否则就用statement格式。

写入机制

binlog的写入时机也非常简单,事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog文件中。

因为一个事务的binlog不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache。

我们可以通过binlog_cache_size参数控制单个线程 binlog cache 大小,如果存储内容超过了这个参数,就要暂存到磁盘(Swap)。

binlog日志刷盘流程如下

上图的 write,是指把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,所以速度比较快上图的 fsync,才是将数据持久化到磁盘的操作

write和fsync的时机,可以由参数sync_binlog控制,默认是1。

为0的时候,表示每次提交事务都只write,由系统自行判断什么时候执行fsync。

虽然性能得到提升,但是机器宕机,page cache里面的 binlog 会丢失。

为了安全起见,可以设置为1,表示每次提交事务都会执行fsync,就如同 redo log 日志刷盘流程 一样。

最后还有一种折中方式,可以设置为N(N>1),表示每次提交事务都write,但累积N个事务后才fsync。

在出现IO瓶颈的场景里,将sync_binlog设置成一个比较大的值,可以提升性能。

同样的,如果机器宕机,会丢失最近N个事务的binlog日志。

两阶段提交

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

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

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

MySQL更新语句的执行流程涉及连接器、分析器、优化器、执行器等功能模块,以及重做日志和归档日志。重做日志采用WAL技术,先写日志再写磁盘,保证数据库异常重启时不会丢失已提交的记录,实现了crash-safe能力。归档日志记录原始逻辑,不同于重做日志的物理记录。文章详细介绍了更新语句的内部流程,包括执行器和InnoDB引擎的交互过程,以及重做日志的两阶段提交。通过对比重做日志和归档日志的特点,读者能够深入了解MySQL的日志系统设计和执行过程。

文章还介绍了MySQL里面最重要的两个日志,即物理日志redo log和逻辑日志binlog。redo log用于保证crash-safe能力。innodb_flush_log_at_trx_commit参数设置成1时,表示每次事务的redo log都直接持久化到磁盘,建议设置成1以保证数据不丢失。sync_binlog参数设置成1时,表示每次事务的binlog都持久化到磁盘,也建议设置成1以保证binlog不丢失。与MySQL日志系统密切相关的“两阶段提交”是跨系统维持数据逻辑一致性时常用的一个方案。

总结:本文深入介绍了MySQL更新语句的执行流程和日志系统设计,包括重做日志和归档日志的特点,以及两阶段提交的重要性。读者可以从中了解MySQL的日志系统设计和执行过程,以及保证数据安全和一致性的关键参数设置建议。

参见:

https://javaguide.cn/database/mysql/mysql-logs.html#%E5%89%8D%E8%A8%80

https://time.geekbang.com/column/article/68633

0 阅读:14

破局之路课程

简介:感谢大家的关注