通过一条更新语句的执行,深入理解 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 log
和 undo 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 pos
到check point
之间的部分:是redo log
空着的部分,用于记录新的操作;check point
到write 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 Log
、 Redo Log
和 Undo Log
。
更新语句的执行流程如下:
- 首先,客户端与 MySQL 服务端建立网络连接,这是连接器的工作;
- 前面我们说过,在一个表上有更新的时候,跟这个表有关的查询缓存会失效,所以这条更新语句就会把表 T 上所有缓存结果都清空,这也就是我们一般不建议使用查询缓存的原因;
- 分析器会通过词法和语法解析知道这是一条更新语句;
- 优化器生成相应的执行计划,选择最优的执行计划;
- 执行器调用引擎取
ID=2
这一行。如果ID=2
这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回; - 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是
N+1
,得到新的一行数据,再调用引擎接口写入这行新数据; - 存储引擎在准备更新
ID=2
的这条数据时,会先把ID=2
和 c 原来的值写入到undo log
文件中,用于提交失败后回滚; - 引擎判断该记录所在的数据页是否可以写入
change buffer
; - 将对数据页的更改写入到
redo log
,将redo log
设置为prepare
状态; - 执行器生成这个操作的
bin log
,并把bin log
写入磁盘; - 执行器调用引擎的提交事务接口,引擎把刚刚写入的
redo log
改成提交(commit
)状态,更新完成。
一个更新语句的大致流程介绍完了,下面我们介绍下 change buffer
和 bin log
。
Change Buffer
changer buffer
是 buffer pool
里的一块内存。作用是减少随机磁盘访问,提升更新性能。
当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer
中,这样就不需要从磁盘中读入这个数据页了。这样就可以减少读磁盘,语句的执行速度会得到明显的提升。
在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer
中与这个页有关的操作。
通过这种方式就能保证这个数据逻辑的正确性。
我们知道了 change buffer
对更新的加速作用,那么,什么条件下可以使用 change buffer
呢?
这个问题涉及到索引相关,我们先给出结论,后面在分析索引的时候在详细介绍。
只有满足了更新的记录的索引是普通索引,并且更新记录的数据页不在内存中,这两个条件,才会将更新操作记录到 change buffer
, change buffer
会在空闲时异步更新到磁盘。
Binlog
前面我们讲过,MySQL 整体来看,其实就有两块:
- 一块是 Server 层,它主要做的是 MySQL 功能层面的事情;
- 还有一块是引擎层,负责存储相关的具体事宜。
上面我们聊到的 redo log
是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog
(归档日志)。
binlog
用于记录数据库执行的写入性操作(不包括查询)信息,以二进制的形式保存在磁盘中。
binlog
是 MySQL 的逻辑日志,并且由 Server 层进行记录,使用任何存储引擎的 MySQL 数据库都会记录 binlog
日志。
逻辑日志:可以简单理解为记录的就是 SQL 语句。
在实际应用中,binlog
的主要使用场景有两个,分别是主从复制和数据恢复。
Binlog 日志格式
binlog
日志有三种格式,分别为:statement
、row
和 minxed
。
statement
:使用statement
格式,binlog
里面记录的就是 SQL 语句的原文;row
:使用row
格式,binlog
里面记录的是 event(Table_map,Write_rows,Delete_rows);minxed
:基于statement
和row
两种模式的混合复制(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 的区别
这两种日志有以下三点不同。
redo log
是 InnoDB 引擎特有的;binlog
是 MySQL 的 Server 层实现的,所有引擎都可以使用。redo log
是物理日志,记录的是「在某个数据页上做了什么修改」;binlog
是逻辑日志,记录的是这个语句的原始逻辑,比如「给 ID=2 这一行的 c 字段加 1 」。redo log
是循环写的,空间固定会用完;binlog
是可以追加写入的。「追加写」是指binlog
文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
有了对这些日志的概念性理解,我们再来看执行器和 InnoDB 引擎在执行这个简单的 update 语句时的内部流程。
执行器和 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
,或者采用反过来的顺序。
我们看看这两种方式会有什么问题。
假设当前 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 log
和 binlog
都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。
总结
本文介绍了 InnoDB 重要的内存结构,包括:缓冲池(buffer pool)和重做日志缓存(redo log buffer)。
还介绍了InnoDB 事务日志:redo log
和 undo log
。这两个日志保证了原子性、一致性和持久性。
我们还分析了更新语句的执行流程以及和查询语句的区别。
最后我们介绍了 Binlog 和两阶段提交。