通过一条更新语句的执行,深入理解 InnoDB 的底层架构

前面通过 一条查询SQL的执行过程 我们知道了 MySQL 的整体架构。对一条查询 SQL 语句的执行流程,也有了整体的了解。

查询语句的执行过程一般是经过连接器、分析器、优化器、执行器等功能模块,最后到达存储引擎。

那么,一条更新语句的执行流程又是怎样的呢?

MySQL 最常用的存储引擎是 InnoDB,我们今天就通过一条更新语句,分析 InnoDB 具体是如何处理的,深入理解下它的架构。

InnoDB

MySQL5.5 版本开始,InnoDB 成为了默认存储引擎。

InnoDB 的特点是支持事务、支持行锁、支持MVCC、外键,提供一致性非锁定读,同时本身设计能够最有效的利用内存和 CPU。

InnoDB 重要的内存结构

在这里插入图片描述

InnoDB存储引擎在内存中有两个非常重要的组件,分别是缓冲池(buffer pool)和重做日志缓存(redo log buffer)。

Buffer Pool

缓冲池其实就是一块内存区域,作用就是弥补磁盘速度较慢对数据库性能产生的影响。

MySQL 表数据是以页为单位,当查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,然后放入到 Buffer Pool 中。

InnoDB 每次查询时,首先判断该页是否在缓冲池中。如果在就直接读取,否则,读取磁盘。减少硬盘 IO 开销,提升性能。

更新表数据的时候,如果 Buffer Pool 里命中数据,就直接在 Buffer Pool 里更新,然后再以特定的机制刷新到磁盘上。

在这里插入图片描述

Buffer Pool 里面不仅有数据页,其缓存的数据页类型有:索引页、数据页、undo 页、插入缓存(insert buffer)、自适应哈希索引、InnoDB 存储的锁信息、数据字典信息等。

在这里插入图片描述
InnoDB 的内存区域除了有缓冲池外,还要重做日志缓存 redo log buffer。在介绍它之前,我们先了解一下 redo log

Redo Log

假设我们把 Buffer Pool 中某个数据页的某条数据修改了,但是硬盘的数据还未同步,此时数据是不一致的,如果 MySQL 宕机了,数据就丢失了。

为了保证数据的持久性,InnoDB 存储引擎加入了 redo log 功能,也叫重做日志。

我们都知道 InnoDB 是支持事务机制的,事务有四个特性:原子性、一致性、隔离性、持久性
事务的隔离性由锁来实现,原子性、一致性、持久性通过 redo logundo log 来完成。

InnoDB 事务日志有两部分组成

  • redo log:重做日志,用来保证事务的原子性和持久性;
  • undo log:回滚日志,用来保证事务的一致性。

Redo Log 的基本概念

redo log 包括两部分:

  • 一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;
  • 二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。

MySQL 每执行一条 DML 语句,先将记录写入 redo log buffer,后续根据写入策略将多个操作记录写到 redo log file

这种先写先写 redo log buffer,再写 redo log file 的技术就是 MySQL 里经常说到的 WAL(Write-Ahead Logging) 技术。

Redo Log

InnoDB 的 redo log 是固定大小的,采用了大小固定,循环写入的方式,当写到结尾时,会回到开头循环写日志。如下面这个图所示:

在这里插入图片描述

  • write pos:表示 redo log 当前记录位置;
  • check point:表示当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件;
  • write poscheck point之间的部分:是 redo log 空着的部分,用于记录新的操作;
  • check pointwrite pos 之间:是 redo log 待落盘的数据页更改记录;
  • write pos 追上 check point 时,这时候不能再执行新的更新,得停下来,同步到磁盘,推动 check point 向前移动,空出位置再记录新的日志。

redo log 是 InnoDB 引擎所特有的,所以我们如果再使用 InnoDB 引擎创建表时,如果数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe

Undo Log

undo log 的两个作用:提供回滚和多个行版本控制(MVCC)。

undo log 是逻辑日志。当 delete 一条记录时,undo log 中会记录一条对应的 insert 记录,反之亦然,当 update 一条记录时,它记录一条对应相反的 update 记录。

当执行 rollback 时,就可以从 undo log 中的逻辑记录读取到相应的内容并进行回滚。

了解了这些我们再来看更新语句的执行流程。

更新语句的执行流程

建表语句:这个表有一个主键 ID 和一个整型字段 c :

mysql> create table T(ID int primary key, c int);

如果要将 ID=2 这一行的值加 1,SQL 语句就会这么写:

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

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

但是和查询流程不一样的是,更新流程还涉及 Change Buffer 和三个重要的日志模块:Bin LogRedo LogUndo Log

更新语句的执行流程如下:

  1. 首先,客户端与 MySQL 服务端建立网络连接,这是连接器的工作;
  2. 前面我们说过,在一个表上有更新的时候,跟这个表有关的查询缓存会失效,所以这条更新语句就会把表 T 上所有缓存结果都清空,这也就是我们一般不建议使用查询缓存的原因;
  3. 分析器会通过词法和语法解析知道这是一条更新语句;
  4. 优化器生成相应的执行计划,选择最优的执行计划;
  5. 执行器调用引擎取 ID=2 这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回;
  6. 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据;
  7. 存储引擎在准备更新 ID=2 的这条数据时,会先把 ID=2 和 c 原来的值写入到 undo log 文件中,用于提交失败后回滚;
  8. 引擎判断该记录所在的数据页是否可以写入 change buffer
  9. 将对数据页的更改写入到 redo log,将 redo log 设置为 prepare 状态;
  10. 执行器生成这个操作的 bin log,并把 bin log 写入磁盘;
  11. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。

一个更新语句的大致流程介绍完了,下面我们介绍下 change bufferbin log

Change Buffer

changer bufferbuffer pool 里的一块内存。作用是减少随机磁盘访问,提升更新性能。

当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。这样就可以减少读磁盘,语句的执行速度会得到明显的提升。

在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。

通过这种方式就能保证这个数据逻辑的正确性。

我们知道了 change buffer 对更新的加速作用,那么,什么条件下可以使用 change buffer 呢?

这个问题涉及到索引相关,我们先给出结论,后面在分析索引的时候在详细介绍。

只有满足了更新的记录的索引是普通索引,并且更新记录的数据页不在内存中,这两个条件,才会将更新操作记录到 change bufferchange buffer会在空闲时异步更新到磁盘。

Binlog

前面我们讲过,MySQL 整体来看,其实就有两块:

  • 一块是 Server 层,它主要做的是 MySQL 功能层面的事情;
  • 还有一块是引擎层,负责存储相关的具体事宜。

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

binlog 用于记录数据库执行的写入性操作(不包括查询)信息,以二进制的形式保存在磁盘中。

binlog 是 MySQL 的逻辑日志,并且由 Server 层进行记录,使用任何存储引擎的 MySQL 数据库都会记录 binlog 日志

逻辑日志:可以简单理解为记录的就是 SQL 语句。

在实际应用中,binlog 的主要使用场景有两个,分别是主从复制和数据恢复

Binlog 日志格式

binlog 日志有三种格式,分别为:statementrowminxed

  • statement:使用 statement 格式,binlog 里面记录的就是 SQL 语句的原文;
  • row:使用 row格式,binlog 里面记录的是 event(Table_map,Write_rows,Delete_rows);
  • minxed:基于 statementrow 两种模式的混合复制(mixed-based replication, MBR),集成了两者的优点。

MySQL 为什么会有两份日志

最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。

而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。

binlog 和 redo log 的区别

这两种日志有以下三点不同。

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

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

执行器和 InnoDB 引擎执行 update 语句时的内部流程

  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 内部执行的,蓝色框表示是在执行器中执行的。

在这里插入图片描述
你可能注意到了,最后三步,将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是「两阶段提交」。

两阶段提交

为什么必须有「两阶段提交」呢?这里我们用反证法来进行解释。

由于 redo logbinlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。

我们看看这两种方式会有什么问题。

假设当前 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 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同。

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

你可能会说,这个概率是不是很低,平时也没有什么动不动就需要恢复临时库的场景呀?

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

简单说,redo logbinlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。

总结

本文介绍了 InnoDB 重要的内存结构,包括:缓冲池(buffer pool)和重做日志缓存(redo log buffer)。

还介绍了InnoDB 事务日志:redo logundo log。这两个日志保证了原子性、一致性和持久性。

我们还分析了更新语句的执行流程以及和查询语句的区别。

最后我们介绍了 Binlog 和两阶段提交。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值