前面我们系统了解了一个查询语句的执行流程,并介绍了执行过程中涉及的处理模块。
相信你还记得,一条查询语句的执行过程一般是经过连接器、分析器、优化器、执行器等功能模块,最后到达存储引擎。
那么,一条更新语句的执行流程又是怎样的呢?
从一个表的一条更新语句说起,下面是这个表的创建语句,这个表有一个主键 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;
更新语句是如何实现的呢?
前面我有跟你介绍过 SQL 语句基本的执行链路,这里我再把那张图拿过来,你也可以先简单看看这个图回顾下。首先,可以确定的说,查询语句的那一套流程,更新语句也是同样会走一遍。
MySQL 的逻辑架构图
你执行语句前要先连接数据库,这是连接器的工作。
前面我们说过,在一个表上有更新的时候,跟这个表有关的查询缓存会失效,所以这条语句就会把表 T 上所有缓存结果都清空。这也就是我们一般不建议使用查询缓存的原因。
接下来,分析器会通过词法和语法解析知道这是一条更新语句。优化器决定要使用 ID 这个索引。然后,执行器负责具体执行,找到这一行,然后更新。
与查询流程不一样的是,更新流程还涉及两个重要的日志模块,它们正是我们今天要讨论的主角:redo log(重做日志)和 binlog(归档日志)。
如果接触 MySQL,那这两个词肯定是绕不过的,我后面的内容里也会不断地和你强调。不过话说回来,redo log 和 binlog 在设计上有很多有意思的地方,这些设计思路也可以用到你自己的程序里。
Redo Log 日志
Redo Log:指事务中修改的任何数据,将最新的数据备份存储的位置(Redo Log),被称为重做日志。(Redo:顾名思义就是重做。以恢复操作为目的,在数据库发生意外时重现操作。)
Innodb存储引擎特有日志
redo log是一种WAL技术应用,英文全称为write-aheading-log
具体来说,当有一条记录需要更新的时候,InnoDB 引擎就会先把记录写到 redo log (先写磁盘,只是写日志是顺序写盘,速度很快)里面,并更新内存,这个时候更新就算完成了。
同时,InnoDB 引擎会在适当的时候,将这个操作记录更新到磁盘binlog里面,而这个更新往往是在系统比较空闲的时候做。
为何有会采用这种技术呢?
因为执行一条SQL更新语句就去写磁盘,需要先在磁盘上查找对应的行,再进行更新,整个过程无论是IO操作,还是查找操作消耗都很大;
因此mysql存储引擎Innodb采用将数据先写入到redo log,然后更新内存,这样就算完成一条更新语句,然后在适当的时候再将数据更新到磁盘中。
同时该技术能够解决crash-safe问题,当系统突然崩溃时,可以通过redo log恢复崩溃前的状态。
Redo Log特性
Redo Log 的生成和释放:
随着事务操作的执行,就会生成Redo Log,在事务提交时会将产生,Redo Log写入Log Buffer,并不是随着事务的提交就立刻写入磁盘文件。
等事务操作的脏页写入到磁盘之后,Redo Log 的使命也就完成了,Redo Log占用的空间就可以重用(被覆盖写入)。
Redo Log 的写入方式:
InnoDB 的 redo log 是固定大小的,比如可以配置为:一组 4 个文件,每个文件的大小是 1GB,那么这块“粉板”总共就可以记录 4GB 的操作。
从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。
两个关键的位置节点: write pos(写入位置) , chech point(擦除位置)
write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。
checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件binlog。
write pos 和 checkpoint 之间的是“粉板”上还空着的部分,可以用来记录新的操作。
如果 write pos 追上 checkpoint,代表内存快写满了,脏页很多,需要强制刷盘,这时候不能再执行新的更新,得停下来先擦掉一些记录,把 checkpoint 推进一下。
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。(崩溃-安全)
Redo Log相关配置参数
每个InnoDB存储引擎至少有1个重做日志文件组(group),每个文件组至少有2个重做日志文件,默认为ib_logfifile0和ib_logfifile1。可以通过下面一组参数控制Redo Log存储:
show variables like '%innodb_log%';Redo Buffer 持久化到 Redo Log 的策略,可通过 Innodb_flush_log_at_trx_commit 设置:0:每秒提交 Redo buffffer ->OS cache -> flflush cache to disk,可能丢失一秒内的事务数据。由后台Master线程每隔 1秒执行一次操作。1(默认值):每次事务提交执行 Redo Buffffer -> OS cache -> flflush cache to disk,最安全,性能最差的方式。2:每次事务提交执行 Redo Buffffer -> OS cache,然后由后台Master线程再每隔1秒执行OS cache -> flflush cache to disk 的操作。一般建议选择取值2,因为 MySQL 挂了数据没有损失,整个服务器挂了才会损失1秒的事务提交数据。
Binlog 归档日志
Redo Log 是属于InnoDB引擎所特有的日志,而MySQL Server也有自己的日志,即 Binarylog(二进制日志),简称Binlog。
Binlog是记录所有数据库表结构变更以及表数据修改的二进制日志,不会记录SELECT和SHOW这类操作。
我想你肯定会问,为什么会有两份日志呢?
因为最开始 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 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
Binlog日志是以事件形式记录
文件记录模式有STATEMENT、ROW和MIXED三种,具体含义如下。
1. ROW(row-based replication, RBR):日志中会记录每一行数据被修改的情况,然后在slave端对相同的数据进行修改。
优点:能清楚记录每一个行数据的修改细节,能完全实现主从数据同步和数据的恢复。
缺点:批量操作,会产生大量的日志,尤其是alter table会让日志暴涨。
2. STATMENT(statement-based replication, SBR):每一条被修改数据的SQL都会记录到master的Binlog中,
slave在复制的时候SQL进程会解析成和原来master端执行过的相同的SQL再次执行。简称SQL语句复制。
优点:日志量小,减少磁盘IO,提升存储和恢复速度
缺点:在某些情况下会导致主从数据不一致,比如last_insert_id()、now()等函数。
3. MIXED(mixed-based replication, MBR):
以上两种模式的混合使用,一般会使用STATEMENT模式保存binlog,对于STATEMENT模式无法复制的操作使用ROW模式保存binlog,MySQL会根据执行的SQL语句选择写入模式。
update 语句时的内部流程
了解了两个日志,那么执行器和 InnoDB 引擎在执行这个简单的 update 语句时,具体是如何使用这两份日志的呢? update 语句时的内部流程。
1.执行器先找引擎取 ID=2 这一行。ID 是主键,引擎直接用树搜索找到这一行。如果 ID=2 这一行所在的数据页(page页 16k大小)本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
2.执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
3.引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
4.执行器生成这个操作的 binlog,并把 binlog 写入磁盘。执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
大致流程如下:
最后三步看上去有点“绕”,将 redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"。
两阶段提交
为什么必须有“两阶段提交”呢?这是为了让redo log 和 binlog 两份日志之间的逻辑一致;
用前面的 update 语句来做例子。假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?
mysql> update T set c=c+1 where ID=2;
由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。我们看看这两种方式会有什么问题。
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,与原库的值不同。
应用设置:
1.redo log 用于保证 crash-safe 能力。innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数我建议你设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。
2.sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数我也建议你设置成 1,这样可以保证 MySQL 异常重启之后 binlog 不丢失。
小结
今天,我介绍了 MySQL 里面最重要的两个日志,即物理日志 redo log 和逻辑日志 binlog。
redo log 用于保证 crash-safe 能力。innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。
这个参数我建议你设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。
sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。
这个参数我也建议你设置成 1,这样可以保证 MySQL 异常重启之后 binlog 不丢失。
我还跟你介绍了与 MySQL 日志系统密切相关的“两阶段提交”。两阶段提交是跨系统维持数据逻辑一致性时常用的一个方案,即使你不做数据库内核开发,日常开发中也有可能会用到。