MySQL实战45讲——02|日志系统:一条SQL更新语句是如何执行的

文章目录

  • 02|日志系统:一条SQL更新语句是如何执行的
    • redo log
    • binlog
    • 两阶段提交
      • 数据恢复的过程
      • 反证法

02|日志系统:一条SQL更新语句是如何执行的

请支持正版:MySQL实战45讲

假如有一个表的创建语句,这个表有一个主键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;

上一篇文章01|基础架构:一条SQL查询语句是如何执行的还是会执行一遍:

  • 先连接数据库
  • 有更新的时候,查询缓存会失效
  • 分析器进行词法分析和语法分析,直到这是一条更新语句
  • 优化器决定使用ID这个索引
  • 执行器负责执行,找到这一行,然后更新c

与查询流程不一样的是,更新流程还涉及到两个重要的日志模块:redo logbinlog

redo log

孔乙己文章中,酒店老板有一个板子,专门用来记录客人的赊账记录,如果赊账的人不多,那么他就可以把顾客名和账目写在板上,如果赊账的人多了,板子记录不下的时候,掌柜还有一个专门记录赊账的账本

如果有人要赊账或者还账的话,掌柜有两种做法:

  • 直接把账本翻出,把这次赊的账加上去或者扣除掉
  • 另一种做法是现在板子上记录下这笔帐,等打烊后再把账本翻出来核算

如果生意红火,那么掌柜肯定选择后者,因为前者操作麻烦,首先要找到这个人的赊账总额的那一条记录,密密麻麻几十页,掌柜要找到那个名字,然后再拿算盘计算,最后再把结果写回账本上

相比之下,还是先在板子上记录一下来的更方便

同样的,MySQL里也有这个问题。如果每次更新操作都要写进磁盘,那么磁盘也得找到对应的那条记录,然后再更新,整个IO过程,查找成本都很高,为了解决这个问题,MySQL的设计者就采用了类似板子的做法

板子和账本配合的过程其实就是MySQL里经常说到的WAL技术,WAL关键的就是先写日志,再写磁盘,也就是先写板子,不忙的时候再写账本

具体来说就是:当有一条记录需要更新的时候,InnoDB引擎就会先把记录写到redo log中,并且更新内存,这个时候更新就算完成了。InnoDB会在合适的时候把这个操作更新到磁盘里,这个更新往往是在系统比较空闲的时候做的

值得注意的是,如果板子满了,掌柜只好先放下手头的工作,把板子里的一部分赊账记录更新到账本里,然后把这些记录从板子上擦掉,为记新帐腾出空间

与此类似的,InnoDB的redo log是固定大小的,比如可以配置一组4个人家,每个文件的大小是1GB,那么这块板子的记录就可以记录4GB的操作,从头开始写,写到末尾又回到开头循环写

binlog

MySQL从整体来看,其实就有两块:一块是Server层,它主要做的是MySQL功能层面的事情,还有一块是引擎层,负责存储具体相关的事宜。上面我们聊到的板子redo log是InnoDB特有的日志,Server层也有自己的日志,也就是binlog

那么有一个显然的问题:为什么有两份日志?

这两种日志有三点不同

  • redolog是InnoDB特有的,binlog是MySQLServer层实现的,所有引擎都可以使用
  • redolog是物理日志,记录的是在某个数据页上做了什么修改,binlog是逻辑日志,记录的是这个语句的原始逻辑,比如说给ID = 2这一行的c字段加1
  • redolog是循环写的,空间固定会用完,binlog是追加写的,也就是写到一定大小以后就会切换下一个,并不会覆盖以前的日志

现在有了基本的概念后,看看执行器和InnoDB引擎在执行这个简单的update语句时的内部流程

  • 执行器先找引擎取ID= 2这一行,ID是主键,引擎直接用树搜索找到这一行,如果ID = 2,这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘中读入内存,然后再返回
  • 执行器拿到引擎给的行数据,然后把这个值 + 1,再调用引擎的接口写入新值
  • 执行器生成binlog,并把binlog写入磁盘
  • 执行器调用引擎提交的事务接口,引擎把刚刚写入的redolog改成提交状态,更新完成
不在
取ID=2这一行
数据页是否在内存
返回行数据
从磁盘中读入内存
将这行的c值+1
写入新行
新行更新到内存
写入redolog且处于prepare阶段
写binlog
提交事务且处于commit状态

你可能注意到了,最后三步有点奇怪,很绕,这里就需要展开说说*两阶段提交**

两阶段提交

为什么要有两阶段提交呢?

数据恢复的过程

这是为了让两份日志之间的逻辑保持一致,在详细说明理由之前,要先说一下数据恢复的过程。现在假设你曾认真备份你的数据库

那么,现在你想回到都指定的某一秒时,你可以这样做:

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

这样你的临时库就和误删之前的线上的库就一样了,然后你可以把表数据哦那个临时库中取出来,按需要恢复到线上库里。

反证法

现在明白了数据恢复的过程,现在可以说明为啥要两阶段提交了,这里采用反证法证明

由于redolog和binlog是两个独立的逻辑,如果不用两阶段提交,那么就是先写redolog再写binlog或者反过来的顺序

下面我们看看会有什么问题:

依然使用前面update的语句作为例子,假设当前id = 2的行,c = 0,再假设执行update过程中在写完第一个日志后,第二个日志还没有写完期间发生了crash,会出现什么情况呢?

  1. 先写redolog,然后写binlog。假设在写完redolog,binlog没写完的时候,MySQL进程异常重启。前面提到过,redolog写完之后,系统即使崩溃,仍然可以把数据恢复回来,所以恢复后c = 1

但是由于binlog没有写完,这时候binlog里可没有这一句,因此备份日志的时候,binlog就没有这条语句。然后如果你要用binlog来恢复临时库的话,由于这条语句的binlog丢失,那么这个临时库就少了一次更新,恢复出来的c值 = 0,与设想的不同

  1. 先写binlog,然后redolog,如果在binlog写完之后crash,由于redolog还没有写完,恢复崩溃后,这个事务无效,因此c = 0。但是binlog已经有这一条日志记录。所以,在之后用binlog恢复临时库的时候,临时库又会多一条更新出来,又和设想的不同

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

你可能会质疑这个事情发生的概率,平时也没有动不动就恢复数据库的场景

其实,不只是这一个场景要用到这个过程,比如说,当你需要扩容的时候,俨然就是需要多搭建一些备用库来增加系统的读能力的时候,现在常见的做法也是全量备份加上应用binlog来实现的,这个不一致就会导致你的线上出现出从数据库不一样的qingk

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

你可能感兴趣的:(MySQL实战45讲,mysql,sql,数据库)