第二章 从更新角度看MySQL 执行流程
- 从一个表的一条更新语句说起,下面是这个表的创建语句,这个表有一个主键 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;
执行流程
- 连接器进行连接数据库的工作
- 分析器会通过词法和语法解析知道这是一条更新语句
- 优化器决定使用ID这个唯一索引
- 执行器调用引擎负责具体执行
更新流程涉及两个重要的日志模块
-
redo log
(重做日志) -
binlog
(归档日志)
重要的日志模块:redo log
什么是 redo log?
-
redo log
是一种 WAL 技术应用,英文全称为write-aheading-log
- 即首先将记录写入到日志里,当数据库空闲时,将记录写入磁盘
- 为何有会采用这种计数呢,因为执行一条SQL更新语句会去写磁盘,然后磁盘会去查找对应的行,再进行更新,整个过程无论是IO操作,还是查找操作消耗都很大
- 因此 MySQL 存储引擎
innodb
采用将数据先写入到redo log
,然后更新内存,这样就算完成一条更新语句,然后在适当的时候再将数据更新到磁盘中。 - 同时该技术能够结果
crash-safe
问题,当系统突然崩溃时,可以通过redo log
恢复崩溃前的状态
先写日志,再写磁盘
- 需要注意的是:“先写日志” 也是先写磁盘,只是写日志是顺序写盘,速度很快
- 先写 redo log 到 log buffer ,具体内容就是针对哪个表空间的哪些页面做了哪些修改
- 然后 log buffer 中的日志内容会在某些时候写到 redo log 日志文件中,比如事务提交时
为什么写 redo log 日志会比刷新内存中的数据页到磁盘快?
- 是因为服务器在启动时就已经给 redo log 日志文件分配好了一块物理上连续的磁盘空间,每次写 redo log 日志都是往文件中追加写,并没有寻址的过程
- 而修改过的数据页要刷新到磁盘的话,可能对应的磁盘空间并不是物理连续的,找起来费劲
总结
- InnoDB引擎先把记录写到 redo log 中,redo log 在哪,它也是在磁盘上,这也是一个写磁盘的过程
- 但是与更新过程不一样的是,更新过程是在磁盘上
随机IO
,费时 - 而写 redo log 是在磁盘上
顺序IO
。效率要高
InnoDB 的 redo log 是固定大小的
- 比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么总共就可以记录 4GB 的操作
- 从头开始写,像循环队列一样,写到末尾又回到开头循环写
-
write pos
是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头 -
checkpoint
是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件
- 擦除记录之前要把记录更新到数据文件,也就是把记录更新到磁盘文件上,这个事情是在系统比较空闲的时候去做的
-
write pos
和check point
之间就是还能写数据的部分,如果追上checkpoint
了,就要先刷写一次,把checkpoint
往后推
crash-safe
- redo log 是 InnoDB 引擎所特有的
- 所以如果我们在使用 InnoDB 引擎创建表时,如果数据库发生异常重启,之前提交的记录都不会丢失
- InnoDB正因为有了 redo log (重做日志),才有了 crash-safe 的能力(即使mysql服务宕机,也不会丢失数据的能力)
重要的日志模块:binlog
redo log 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog(归档日志)
为什么会有两份日志呢?
- 因为最开始 MySQL 里并没有 InnoDB 引擎。MySQL 自带的引擎是 MyISAM,但是 MyISAM 没有 crash-safe 的能力,binlog 日志只能用于归档。
- 而 InnoDB 是另一个公司以插件形式引入 MySQL 的,既然只依靠 binlog 是没有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系统——也就是 redo log 来实现 crash-safe 能力。
binlog
为什么没有 crash_safe
的能力呢?
- 写入方式的问题,binlog 是追加写,crash 时不能判定 binlog 中哪些内容是已经写入到磁盘,哪些还没被写入
- 而 redolog 是循环写,从 check point 到 write pos 间的内容都是未写入到磁盘的
- 假如现在重新设计 MySQL,只用一个 binlog 实现cash_safe,只不过 binlog 中也要加入
check point
,数据库故障重启后,binlogcheck point
之后的 sql 都重放一遍。但是这样做让 binlog 耦合的功能太多。
redo log 和 binlog 有什么区别?
- 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
写入完成后,由prepare
变为commit
提交状态 - 两阶段提交都是为了保障一致性,这里是保障
redo log
和binlog
的一致性,最终是保障db数据的一致性
为什么日志需要 “两阶段提交
” ?
- 如果不使用两阶段提交,先写 redo log 后写 binlog,在写完 redo log 后,写 binlog 的时候发生 crash。
- 数据库恢复回来后,数据没丢失,因为能根据 redo log 恢复回来,但是这个操作却少了一个 binlog。
- 而在进行数据库备份的时候使用的是 binlog,所以备份的数据里面就丢失了这次更改,以后在使用这个备份恢复的时候,自然恢复回来的数据就不对。
总结
- redo log 用于保证 crash-safe 能力。
innodb_flush_log_at_trx_commit
这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数我建议你设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。 - sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数我也建议你设置成 1,这样可以保证 MySQL 异常重启之后 binlog 不丢失。
- redo log 记录的是磁盘上数据的物理变化,binlog 记录的是当时所执行的高级编程语言 (sql语句)