《MySQL实战》

日志系统

一条查询sql语句执行一般是经过连接器,查询缓存,分析器,优化器,执行器等功能模块,最后到达存储引擎。

那么一条更新sql语句的执行流程呢?

如今的MySQL,假如碰到一些生产环境的事故,数据库数据发生巨大错误,这种后果是不允许接受的,但是我们无法保证不出现问题,那么让数据回到错误发生之前就好了,mysql恰恰可以恢复到任何一个时刻(当然要做一些备份操作),下面我们可以讲一下其中的原理。

我们从一个表的一条更新数据说起

mysql> create table T(ID int primary key, c int);
复制代码

如果将ID=2这一行的值+1

mysql> update T set c=c+1 where ID=2;
复制代码

更新操作走的仍然是查询操作的基本链路,前面我们说过在一个表上有更新的时候,跟这个表相关的查询缓存就是失效,所以我们不建议用查询缓存。

接下来,分析器会通过词法和语法解析知道这是一条更新语句。优化器决定要用使用ID这个索引。然后执行器负责执行这一行的更新。

与查询不同的是,更新操作还涉及两个很重要的日志模块,redo log(重做日志),binlog(归档日志)。

redo log

举个例子,如果有人赊账或者还账操作,掌柜一般有两种做法,主要是针对’忙不忙‘

  • 一种是直接把账本拿出来,查找到那个人进行操作
  • 另一种就是先在一个板子上记下这次账,等打样后再进行记录

同样再mysql中,如果每次的更新操作都需要将操作写进磁盘,然后磁盘在写入时也需要找到对应的数据,然后再更新,整个io成本,查找成本都不可忽视。为了解决这个问题,mysql也进行了类似掌柜的操作。

板子和账本配合的操作再MySQL中就是WAL技术,wait ahead logging,先写日志,再写磁盘。

具体来说,当有一条记录需要更新的时候,innodb引擎需要先将记录写道redo log中,并且更新内存,这个时候其实更新记录就已经完成了。innodb会在恰当的时候将这条记录写道磁盘里,往往是系统比较空闲的时候。

但是如果今天真的“很忙”,redo log满了?那么我们还是需要先将数据先写进binlog,为redo log腾出空间,因为redo log在innodb中是固定大小的。比如下面一组4个文件,每个文件的大小是1gb,那么这块redo log就总共可以记录4gb的操作。redo log是从头开始写,写到末尾就回到开头循环写,如下面这幅图

write pos是当前记录的位置,一边写一边后移,写到3后,就会回到0继续写。

checkpoint是擦除的位置(写到磁盘后需要擦除),擦除之前要将记录更新到数据文件。

如果checkpoint被write pos追上了,就说明需要停一下redo log,先进行binlog。

有了redo log,innodb就可以保证即使数据库发生异常重启,之前的记录都不会丢失,这个能力称为crash-safe。比如掌柜忘了打烊后整理到账本,但是板子上的数据仍然存在,可以随时恢复。

binlog

从MySQL整体来看,就有两块,server层,它主要做mysql功能层面的事情;引擎层,负责存储相关的具体事情。而server层也有自己的binlog日志。

为什么会有两份日志呢?

因为最开始MySQL里并没有InnoDB引擎。MySQL自带的引擎是MyISAM,但是MyISAM没有crash-safe的能力,binlog日志只能用于归档。而InnoDB是另一个公司以插件形式引入MySQL的,既然只依靠binlog是没有crash-safe能力的,所以InnoDB使用另外一套日志系统——也就是redo log来实现crash-safe能力。

这两种日志有以下三点不同。

  • redo log是innodb独有的;binlog是server层的,引擎通用。
  • redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,比如给id=2这一行的字段加2
  • redo log是循环写的,空间固定会被用完;binlog是可以追加写入的。追加写是指binlog文件到一定大小后会自动切换到下一个,并不会覆盖之前的。

我们再来看执行器和InnoDB引擎在执行这个简单的update语句时的内部流程。

1.执行器先找引擎取到id=2这一行。id是主键,引擎直接用树搜索找到这一行,如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。

2.执行器拿到引擎给的行数据,把这个值+1,得到一行新数据,在调用引擎接口写入这行数据。

3.引擎将这行数据写入内存,同时将这个操作记录更新到redo log日志,此时redo log处于prepare状态。然后告知执行器完成,关闭事务。

4.执行器生成这个操作的binlog,并将binlog写入磁盘

5.执行器调用引擎接口提交事务接口,引擎把刚才的redo log改成commit状态,更新完成

这个update语句的执行流程图,图中浅色框表示是在InnoDB内部执行的,深色框表示是在执行器中执行的。

最后三步看上去有点“绕”,将redo log的写入拆成了两个步骤:prepare和commit,这就是"两阶段提交"。

两阶段提交

前面我们说过了,binlog会记录所有的逻辑操作,并且是采用“追加写”的形式。如果你的DBA承诺说半个月内可以恢复,那么备份系统中一定会保存最近半个月的所有binlog,同时系统会定期做整库备份。这里的“定期”取决于系统的重要性,可以是一天一备,也可以是一周一备。

当需要恢复到指定的某一秒时,比如某天下午两点发现中午十二点有一次误删表,需要找回数据,那你可以这么做:

  • 首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库;
  • 然后,从备份的时间点开始,将备份的binlog依次取出来,重放到中午误删表之前的那个时刻。

这样你的临时库就跟误删之前的线上库一样了,然后你可以把表数据从临时库取出来,按需要恢复到线上库去。

好了,说完了数据恢复过程,我们回来说说,为什么日志需要“两阶段提交”。这里不妨用反证法来进行解释。

由于redo log和binlog是两个独立的逻辑,如果不用两阶段提交,要么就是先写完redo log再写binlog,或者采用反过来的顺序。我们看看这两种方式会有什么问题。

仍然用前面的update语句来做例子。假设当前ID=2的行,字段c的值是0,再假设执行update语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了crash,会出现什么情况呢?

1.先redo log再binlog,当redo

log提交完成了时候crash了,这时候binlog还没有写完,这时候可以恢复数据,但是binlog缺少一条记录,当我们进行备库恢复的时候,就会发现其实数据并不对

2.先binlog再redo log

当binlog写完redo log未写完crash,崩溃后这个事务没法恢复,所以数据不会改变,但是binlog已经改变了,所以还是那个备库恢复问题,会多出一个事务来

简单说,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不丢失。

转载于:https://juejin.im/post/5cdaa356f265da03aa4dcbf9

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值