恒奥中心,下午7点。度过了浑噩的一周,感觉有好几年都没有这样感冒发烧了,周一又把秋裤穿了回来。现在回想起曾经提醒我穿秋裤的人们,想必都经历过真正的寒冷,而马路上的年轻人们,用裸露的脚脖子对世界发泄着自己的青春,而我,在两个阵营中摇摆不定,就像四月份北京的天气,丝毫没有自己的立场。周一zumba课程暂停,正好可以趁机恢复体力,B1培训顺利通过吧。
一条简单查询的过程已经在上篇中介绍了,准确的说是执行流程,以及执行流程中涉及到的各个模块:连接器、分析器、优化器、执行器等,最后到达存储引擎。那么一条更新语句的执行流程是不是也是类似的呢,看一个简单的更新语句吧
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。
记得以前林老师曾用过孔乙己里的粉板的例子,非常形象。酒店掌柜有一个粉板专门用于临时记录一下客人的赊账记录,如果赊账的人不多,就直接把名字和赊账账目记在粉板上了,粉板可以记录一天两天,但三天、四天之后,总会记不下的,而且如果赊账的人多,粉板当天可能就会记录不下,所以掌柜肯定会有一个账本来专门记录总的赊账账目。
如果有人来赊账或还账,掌柜有两种选择:
当生意很忙时,掌柜肯定会选择第二种方法,如果每次赊账还账都要找出账本记录,不仅耗费功夫,还容易出错,效率很低。还是直接在粉板上记一下方便,根据小店的客流量制定一个核算账本的周期,定期把粉板上的记录同步记录或更新到账本里。
同样地,如果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。
再回到那张架构老图,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 的功能。
mysql> update T set c=c+1 where ID=2;
Ok,有了两个日志的概念,我们再来看一下执行器和 InnoDB 引擎对于这个简单更新的执行流程
为啥现在提到两阶段提交?
在👆上面的执行流程中后三步,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,会怎么样呢?
先写 redo log 再写 binlog:此时 redo log 已经写完了,binlog写到一半发生了 crash,MySQL异常重启,但 redo log 已经写完了,系统即使崩溃也能把数据恢复回来,所以恢复后 c =1
但由于 binlog 没写完就 crash 了,所以 binlog 里还没有这条逻辑,那么后面备份日志的时候自然也就没有这条语句。这样,如果需要使用 binlog 恢复临时库时,由于这个语句的 binlog 缺失,恢复出来的操作少了这一次更新,c的值就是0,与原库不同了
先写 binlog 再写 redo log:如果 binlog 已经写完了,写 redo log 时崩溃了, 因为 redo log 没写,崩溃恢复以后这个事务无效,所以这一行的 c 为 0,但 binlog 里已经写了这条 update 语句的逻辑,所以从 binlog 恢复出来的时候就多出一个事务,恢复出来的值为 1,与原库不同
所以如果不用两阶段提交,那么数据库的状态就有可能和它的日志恢复出来的库的状态不一样。
目前在生产系统中还没有遇到要从临时库恢复线上数据的情况,只是在测试环境下进行了相关的演练。未来可能会遇到的场景:
这样的场景我觉得可能都比较适合最近的全量备份 + binlog 的方法实现。
redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致
innodb_flush_log_at_trx_commit
设置为 1 时,表示每次事务的 redo log 都直接持久化到磁盘,建议开启,保证 MySQL 异常重启之后数据不丢失,保证 crash-safe 能力。
sync_binlog
设置为 1 时,表示每次事务的 binlog 都持久化到磁盘,建议开启,保证 MySQL 异常重启后 binlog 不丢失。
###