上一篇我们知道了一条sql查询语句是则如何执行的,接下来我们看看一条sql更新语句是如何执行的。
首先,可以确定的说,查询语句的那一套流程,更新语句也是同样会走一遍。
连接数据库,这是连接器的工作。上一篇说过,查询缓存会在表更新的时候清空这张表所有的缓存。接下来分析器通过此法和语法解析确定这是一条更新语句。优化器决定索引。然后,执行器负责执行,调用存储引擎接口更新数据。
与查询流程不一样的是,更新流程还涉及到两个重要的日志模块:redo log(重做日志)和 bin log(归档日志)。
重要的日志模块:redo log
如果我们每一次更新都需要写进磁盘,然后磁盘也要找到对应的那条记录,最后更新。这个过程的 IO、查找成本都很高。
为了解决这个问题,MySQL的设计者引入了 WAL 技术,WAL 的全称是 Write-Ahead Logging,它的关键点就是先写日志,再写磁盘。
具体来说,当一条记录需要更新的时候,InnoDB 引擎会先把记录写入 redo log,并更新内存,这个时候更新就算完成了。同时,InnoDB 会在适当的时候,将这个记录更新到磁盘,而这个更新往往是系统比较空闲的时候做。
InnoDB 的 redo log 的大小是固定的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么总共可以记录 4GB 的操作。从头开始写,写到末尾了,这时就不能再执行新的更新,得先停下来更新开头的一部分记录到磁盘并擦除 redo log 中更新过的记录,然后从头开始写。
有了 redo log。InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力成为 crash-safe。
重要的日志模块:binlog
上一篇讲过,MySQL大体分为两块:一块是 server 层,它主要做的是 MySQL 功能层面的事情;另一块是引擎层,负责存储相关的具体事宜。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 是逻辑日志,记录的是这个语句的原始逻辑。
- redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,不会覆盖以前的日是。
理解了两种日志的概念后,我们来看看解释器和 InnoDB 引擎在执行下面这个 update 语句时的流程。
update T set c = c + 1 where ID = 2
- 执行器先从存储引擎中找到 ID = 2 的记录。ID 是主键,存储引擎直接用树搜索到这一条记录。如果 ID = 2 这条记录在内存中,直接返回给执行器;如果没在内存中,需要先从磁盘中读入到内存,然后再返回。
- 执行器拿到记录后,把 c 的值加上 1,然后调用存储引擎更新这条记录。
- 搜索引擎将这条记录更新到内存中,同时将这个更新操作记录到 redo log 中,此时的 redo log 处于 prepare(准备) 状态。然后告诉执行器执行完成,可以随时提交事务。
- 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
- 执行器调用搜索引擎的提交事务接口,搜索引擎把刚刚把 redo log 的 prepare 状态修改为 commit 状态,更新完成。
上面五个步骤,把 redo log 的写入拆分成了两个步骤:prepare 和 commit。这就是“两阶段提交”。
两阶段提交
为什么要“两阶段提交”呢?这是为了让两份日志之间的逻辑一致。
如果某天下午两点发现中午十二点有一次误删表,需要找回数据,那你可以这么做:
- 首先,找到最近一次的全量备份,如果运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库。
- 然后,从备份的时间点开始,将备份的 binlog 依次取出,重放到中午误删表之前的那个时刻。
这样你的临时库就跟误删表之前的线上库一样了,然后你可以把表数据从临时库取出来,按需恢复到线上库去。
理解了数据恢复过程,我们回来看看,为什么日志需要“两阶段提交”。这里我们用反证法来进行解释。
由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么先写完 redo log 在写 binlog,或者采用反过来的顺序。
- 先写 redo log 后写 binlog。假设在写完 redo log,binlog 还没写完的时候,系统发生了崩溃,binlog 中没有记录。此时恢复了系统,由于写了 redo log,记录的更新操作是成功的,但是没有 binlog 记录,如果需要用 binlog 来恢复临时库的话,这个临时库就会少一次更新,恢复出来的这一行与原库的值不同。
- 先写 binlog 后写 redo log。如果在写完 binlog 后系统崩溃,由于还没写 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 不会丢失。