MySQL更新语句执行过程之日志系统
前一节我们说了一个查询语句的执行过程。这一篇,来说一个更新语句是如何执行的,其中更新语句的大体流程和查询语句的执行流程大体一样。
它们的不同点就是更新语句执行流程中还涉及两个重要的日志。redo log(重做日志)和 binlog(归档日志)
重做日志-redolog
在说redolog之前先想一个问题:如果你是一个餐馆的老板,每天客人络绎不绝,赊账的也大有人在。此时你要如何保证你可以快速准确的所有客人的记录?给你提供三种方式:
1:用脑子记住;
2:在专门记录赊账的总本上进行操作;
3:使用一个小本子暂时记录;
redolog是一个物理日志。如果我们将数据库比作记账本,那redolog就是临时记账本。那为什么要有它呢?
两个功能
其一:提高更新速度;例如总的记账本,里面的数据很多,所以如果我们每次记账都要去总账里面寻找的话,那记账速度可想而知是多么的慢。特别是业务还特别的忙的时候,那就可能造成数据库崩溃,数据丢失等问题。而我们把账目暂时先记在临时的账本上,等业务不忙的时候再将临时账本上的数据持久到总帐本中,这样就有效的提高了MySQL的运行速度。
其二:防止数据丢失;redolog分为两部分,一是内存中的日志缓冲(redo log buffer),该部分日志是易失性的;二是磁盘上的重做日志文件(redo log file),该部分日志是持久的。
redolog就像我们我们的临时账本一样,它的储存空间是有限的,它的数据是进行的循环写就像一个圆圈,从头开始写,写到末尾结束,如果写到末尾了,就停止写入,将其中的一部分数据先持久化到磁盘,然后在redolog中将这部分数据抹掉。再继续进行写。
有了redolog之后,innoDB就可以保证即使数据库发生异常重启,所有已经提交的事务的数据仍然存在,所有没有提交的事务的数据自动回滚,这个能力称为crash-safe。就是营业一天后即使临时账本的数据还没来得及写入总帐本就突然停业了几天,之后即使掌柜忘记了,恢复营业之后也可以通过总帐本和临时账本的数据明确赊账数目。
我们要知道,以上的提高速度和防止丢失都是相对来说的,第一种方式速度快,但是容易丢失数据。第二种不会丢失数据,但是速度太慢了。所以我们取一种折中的方式。
归档日志-binlog
MySQL整体来说就两个部分,一个server层,主要做MySQL功能层面的事情,还有一块引擎层,负责储存相关的具体事宜。上面我们说的redolog就是innoDB引擎特有的日志。而server层也有自己的日志,就是binlog(归档日志)
为什么要两个日志
大家也许疑问了,为什么需要两个日志
因为最开始MySQL里面并没有InnoDB引擎。MySQL自带的引擎是MyISAM,但是MyISAM没有crash-safe的能力,binlog日志只能用于归档。而InnoDB是另一个公司以插件的形式引入MySQL的,既然只依靠binlog是没有crash-safe能力的,所以InnoDB使用另外一套日志系统–也就是redo log来实现crash-safe能力。
为什么binlog没有crash-safe能力
又有了一个问题:为什么binlog没有crash-safe能力?
因为binlog在设计的时候是追加写的,也就是binlog记录的全量的日志。通过binlog并不能确定那些已经数据复盘那些操作没有复盘。所以当数据库异常重启后也就无法确定需要将那些数据复盘到磁盘。而redolog只要是复盘了的数据就会在redolog中删除掉。所以redolog中的数据全是没有复盘的。所以redolog有crash-safe能力而binlog没有。
两种日志的不同
两种日志有以下三点不同。
1,redo log是InnoDB引擎特有的,binlog是MySQL的Server层实现的,所有引擎都可以使用。
2,redo log是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给id=2这一行的c字段加1”。
3,redo log是循环写,空间固定会用完,binlog是可以追加写的,就是binlog文件写到一定大小之后就会切换到下一个,并不会覆盖以前的日志。
执行一个简单的 update 语句时的内部流程
接下来我们看一下执行一个简单的 update 语句时的内部流程。
流程
1,首先执行器先找引擎取ID=2这一行,ID是主键,引擎直接用树搜索找到这一行。如果ID=2这一行所在的数据页本来就在内存中,就直接返回给执行器,否则,需要先从磁盘读入内存,然后再返回。
2,执行器拿到引擎给的行数据,把这个值加上1,比如原来是N,现在就是N+1,得到新的一行数据,再调用引擎接口写入这一行新数据。
3,引擎将这一行新数据更新到内存中,同时将这个更新操作记录到redo log里面,此时redo log处于prepare状态。然后告知执行器执行完成了,随时可以提交事务。
4,执行器生成这个操作的binlog,并把binlog写入磁盘。
5,执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log 改成提交状态,更新完成。
流程图:
两阶段提交
你一定注意到了最后三步很繁琐。redo log需要先处于prepare状态,最后又变为commit状态。它的提交被拆成了两步。这个过程就是两阶段提交。
要说两阶段提交就要说一个很酷的功能,那就是将数据库的数据恢复到某个时间状态。就是DBA说的让数据库恢复到半个月内任意一秒的状态
要将数据库恢复到半个月内的任意一秒的状态,就要使用redo log 和 binlog两个日志了。
如果DBA说可以恢复到半个月内的任意一秒的状态,那至少要保存半个月的binlog。并且数据库还会定期进行全量备份,至于备份的频率就取决于数据的重要性了。
首先找到最近的一次全量备份,然后找到之后的binlog。在全量备份的基础上使用binlog执行到你指定的时间点,就可以实现让数据库恢复到半个月内的任意一秒的状态。
为什么要两阶段提交
说完这个功能我们再来说为什么要两阶段提交。
我们来进行反证法,假设是先提交redo log 然后再提交binlog。
此时,如果在redo log 提交之后,binlog 提交之前数据库发生了异常重启,就导致了binlog比redo log少了一条更新记录。假如是给id = 3,c = 0的这条数据的c+1;之后用binlog进行数据恢复的时候就会导致恢复的数据中c=0,但是原库中应该是c=1。
假设是先提交binlog再提交redo log。
此时,如果binlog提交之后,redo log提交之前数据库发生了异常重启。那binlog已经记录的更新逻辑,但是redo log中没更新,原库中的c还是0,之后用binlog进行恢复的时候,恢复的库中c会等于1,又导致了与原库数据不一致。
所以我们要进行两阶段提交,redo log写完成之后,处于prepare状态,然后写binlog 最后再将redo log改为commit状态,这样的话,如果中途数据库异常重启了,重启之后发现redo log处于prepare状态,那就去检测binlog是否已经写入,如果已经写入了就执行提交,如果binlog没有写入,redo log 就进行回滚。这样就保证了数据的一致性。