<
MySQL-Note-2:从update语句看日志系统
>
上一篇

MySQL-Note-3:Transaction Isolation
下一篇

MySQL-Note-1:一条SQL是如何执行的
每次开始前的絮叨

恒奥中心,下午7点。度过了浑噩的一周,感觉有好几年都没有这样感冒发烧了,周一又把秋裤穿了回来。现在回想起曾经提醒我穿秋裤的人们,想必都经历过真正的寒冷,而马路上的年轻人们,用裸露的脚脖子对世界发泄着自己的青春,而我,在两个阵营中摇摆不定,就像四月份北京的天气,丝毫没有自己的立场。周一zumba课程暂停,正好可以趁机恢复体力,B1培训顺利通过吧。

查询 vs. 更新 : 执行流程比较

一条简单查询的过程已经在上篇中介绍了,准确的说是执行流程,以及执行流程中涉及到的各个模块:连接器、分析器、优化器、执行器等,最后到达存储引擎。那么一条更新语句的执行流程是不是也是类似的呢,看一个简单的更新语句吧

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

比如创建一个最简单的表,有一个ID字段作为主键,有一个整型字段c,我们将ID=2这条记录的值加1

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

的确与查询类似,之前查询走过的流程更新也会走一遍

执行链路第一步还是连接数据库,由连接器解决。

之前说查询缓存时,如果该表上有更新操作,会导致这个表相关的查询缓存全部失效 ,所以这条语句会把这个表T的所有缓存结果清空。

分析器通过词法语法了解到这是一条更新语句,优化器决定使用ID这个索引,执行器操作引擎找到这一行记录,然后进行更新。

但与查询不同的是,更新还涉及了两个重要的日志模块,binlog 和 redo log。

redo log

记得以前林老师曾用过孔乙己里的粉板的例子,非常形象。酒店掌柜有一个粉板专门用于临时记录一下客人的赊账记录,如果赊账的人不多,就直接把名字和赊账账目记在粉板上了,粉板可以记录一天两天,但三天、四天之后,总会记不下的,而且如果赊账的人多,粉板当天可能就会记录不下,所以掌柜肯定会有一个账本来专门记录总的赊账账目。

如果有人来赊账或还账,掌柜有两种选择:

当生意很忙时,掌柜肯定会选择第二种方法,如果每次赊账还账都要找出账本记录,不仅耗费功夫,还容易出错,效率很低。还是直接在粉板上记一下方便,根据小店的客流量制定一个核算账本的周期,定期把粉板上的记录同步记录或更新到账本里。

同样地,如果MySQL中每一次更新都要进行磁盘读写请求,然后磁盘也要找到对应记录,最后再更新,整个过程IO成本、查找成本都很高,所以就有了MySQL中的WAL(Write Ahead Logging)技术,即先写日志、再落盘,这与先记到粉板再记到账本是一个道理。

当有一条记录要更新时,InnoDB 引擎会把记录先写进 redo log,并更新内存,这个时候我们认为更新就算是完成了,等到合适的时候,InnoDB引擎会将这个操作记录写到磁盘里。合适的时候往往就是系统相对空闲的时候,这与掌柜选择打烊之后对账又是一个道理。

但我们刚才说过一种情况,就是当天赊账的特别多,还没等打烊呢粉板就已经记满了,这时掌柜的只能先放下手上的活儿把粉板上的一部分记录先更新进账本里,把这些记录从粉板上擦掉,给新的赊账留出地儿。

那么相对应到 MySQL 中,redo log 这块”粉板“同样也不是无限大的,是固定大小的,但可以配置。比如可以配置为一组4个文件,每个文件的大小为1GB ,那么总共可以记录4GB的操作,且循环利用,比如从头开始写,写到末尾就又会回到开头循环写。

innodb_log_files_in_group=4
innodb_log_file_size=1020M

write pos 是当前记录的位置,一边写一边后移,写到第3号日志文件末尾后就会回到0号日志文件开头,check point 是当前要擦除的位置,同样往后推移且循环,擦除记录前要把记录更新到数据文件。write pos 与 check point 之间的地方就是当前还可以使用的空间,可以用来记录新的操作,如果 write pos 追上 check point,就代表粉板已经满了,这时不能直接再进行新的更新了,先停下来擦掉一些记录,将 check point 向后推进一下。

之所以InnoDB可以保证数据库发生异常重启时,之前提交的记录都不会丢失,正是因为有 redo log 这快粉板已经把最新的操作记录下来了,也就是 crash-safe。

binlog

再回到那张架构老图,MySQL 总体上分为的两层

上面的 redo log 就是 InnoDB 引擎特有日志,而我们所说的 binlog 就是 Server 层自己的日志,也就是归档日志。

为什么会有两份日志?

MySQL 最初并没有 InnoDB 引擎,自带的引擎为 MyISAM,但 MyISAM没有 crash-safe 的能力,binlog 日志只能用于归档,而 InnoDB 是由另一家公司(Innobase Oy,后被 Oracle 收购)以插件形式引入的,那么既然只靠binlog没有 crash-safe 的能力,所以 InnoDB 才使用另一套日志系统,也就是 redo log 来实现 crash-safe 的功能。

与redo-log不同的地方

再看执行流程

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

Ok,有了两个日志的概念,我们再来看一下执行器和 InnoDB 引擎对于这个简单更新的执行流程

  1. 执行器先找引擎取 ID=2 这一行,ID 是主键,引擎直接用树搜索找到这一行,如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器,否则先从磁盘读入内存再返回。
  2. 执行器拿到引擎给的行数据,把这个值加1,得到一行新的数据,再调用引擎接口写入这行新数据。
  3. 引擎将这行数据更新到内存中,同时将这个更新操作记录到 redo log,此时 redo log 处于 prepare 状态,然后告知执行器执行完成了,随时可以提交事务。
  4. 执行器生成这个操作的 binlog,并把binlog写入磁盘。
  5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交 (commit) 状态,更新完成。

两阶段提交

为啥现在提到两阶段提交?

在👆上面的执行流程中后三步,prepare状态与commit的转变就是两阶段提交的体现。

为啥要两阶段提交呢?

当然是为了使两份日志之间的逻辑一致,这里还是用DBA人员(我不是DBA,假装一下。。。)长谈的问题来说明,如何使数据库恢复到一段时间内任意一秒的状态呢?

前面说 binlog 的时候,我们知道,binlog 会记录所有逻辑操作,支持追加写的方式。如果运行部门的同事要求半个月内的数据可以恢复,那么备份系统中一定会保存最近半个月的所有 binlog,同时系统会定期做整库备份。当然了备整库肯定会涉及一些锁操作,那么定期的频率就取决于系统的重要性与业务模式,可以一天一备,也可以一周一备。

当要恢复到指定的某一秒时,比如某个周末上线发现误删了一张表,要找回数据:

这样,这个临时库就和误操作之前的生产库一样了,然后就可以把表数据从临时库里取出来,按需要恢复到线上库了。这就是恢复过程,与两阶段提交有什么关系?

由于 redo log 与 binlog 是两个独立的逻辑过程,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog 或者先写 binlog 再写 redo log,我们看看都有什么问题。

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

假设 ID=2 的这个字段 c 的值当前为 0 ,考虑这样的情形,执行update语句过程中,当第一个日志写完后,写第二个日志期间发生了 crash,会怎么样呢?

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

目前在生产系统中还没有遇到要从临时库恢复线上数据的情况,只是在测试环境下进行了相关的演练。未来可能会遇到的场景:

这样的场景我觉得可能都比较适合最近的全量备份 + binlog 的方法实现。

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

持久化参数

innodb_flush_log_at_trx_commit设置为 1 时,表示每次事务的 redo log 都直接持久化到磁盘,建议开启,保证 MySQL 异常重启之后数据不丢失,保证 crash-safe 能力。

sync_binlog设置为 1 时,表示每次事务的 binlog 都持久化到磁盘,建议开启,保证 MySQL 异常重启后 binlog 不丢失。

###

Top
Foot