引言
通过上篇,我们了解到一条查询SQL是如何在Mysql内部执行的,除了数据的查询,Mysql还支持数据的修改。那么今天我们看一看一条修改SQL又是如何执行的,哪些流程和查询一样,哪些又是修改独有的流程。
本文结构
阅读体验
📚 全文字数 : 4k+
⏳ 阅读时长 : 6min
Mysql的应用架构
我们先来回顾下Mysql的应用架构有哪些部分组成,一条查询SQL经历了连接器、分析器、优化器、执行器,最后将结果集返回。
sql
insert geektao (id, name) value (1, 'geektao');
update geektao set name = 'geektao2' where id = 1;
一条修改SQL同样需要经历这些步骤:
- 首先客户端通过连接器与Mysql服务器建立会话连接;
- 如果查询缓存中有这张表的缓存,那么就把表的缓存全部删掉;
- 分析器通过对SQL进行词法、语法、语义分析,知道这是一条修改SQL,创建语法树;
- 优化器通过语法树创建执行计划,确定修改哪张表、使用哪个索引,先调用存储引擎的查询接口找到这条数据,然后根据修改SQL调用存储引擎的接口进行数据的修改;
- 执行器返回操作影响行数。
是不是看起来没有什么特别的地方?以InnoDB举例,它是将表的数据通过B+树(之后的篇章会详细讲)组织到磁盘里的,而磁盘是随机读写的,为了提高效率,每次调用操作系统的写入磁盘操作,操作系统并不会立即将数据写入磁盘,为了保证数据的一致性,Mysql每进行一次修改操作,都需要进行刷盘。磁盘的写入属于IO操作,IO是很耗时的,并且磁盘又是随机读写,每次刷盘的位置都不一样,这样一是严重降低机械硬盘的使用寿命,二是对于更新操作这是一个很要命的性能瓶颈。
Mysql内部是如何进行优化的呢?先给大家讲一个小故事😄:
我二叔在村里经营着一家小卖铺,每卖出一个商品都需要记录一下商品卖了多少还剩多少,以便去进货时知道需要进什么货了。
为了方便记录,二叔整了一个本子,上边记录了所有的商品及其剩余的数量。一开始生意平平庸庸,二叔每次卖出一个商品,就直接将商品剩余的数量进行修改,还算忙的过来,随着二叔进货的品类越来越多,生意逐渐好了起来,记录的本子从2页变成了20页。
人变得越来越多,但是收入却没有明显的增多,二叔很是费解。突然有一天有个人大喊:"付个账,怎么这么慢啊,不买了!",原来随着货物越来越多,买货的人越来越多,二叔每次卖出个商品还得按着本子不停地翻啊翻,先翻到第一页,一行行看有没有这个商品,没有在翻到下一页,在一行行找有没有这个商品,直到找到为止,记录剩余数量,结账,这能不慢吗?
后来二叔又找了一个小册子,这个小册子卖出一个商品,用铅笔只记录该商品和卖出的数量,一行行记录,因为不需要去原来的本子里查找,只需要按照顺序一条条记录就行了,效率一下子就提高了,每天的收入也越来越多。小卖铺打烊后,二叔在根据小册子中记录的内容,再更新到大本子中进行归档,然后把小册子中的记录擦掉。
再后来,生意越来越好,小册子还没到打烊就用完了,二叔这时候只能停下来,把小册子中已经记录的商品更新到大本子中,然后擦掉这些记录,在从头开始记录。(有同学可能会说,在找一个小册子记录不就行了,还不用停止工作。这里只是为了大家方便理解,因为小册子是临时记录的,只要记录被更新到大本子中,就可以擦掉小册子中的记录了,所以限定就只用一个小册子就够了)。
重做日志 redo log
对于二叔来说,先记小册子,等晚上不忙的时候再记录到大本子中。InnoDB也是类似二叔的思路,在InnoDB官方术语叫做WAL(Write-Ahead Logging),先写日志,再写磁盘。
InnoDB会先将需要更新的记录保存到redo log(小册子)中,并更新内存,这时候更新操作就算完成了。然后,InnoDB会在系统比较空闲的时候(打烊之后),将这个操作记录更新到磁盘(大本子)里。
那redo log会不会记录满呢?会的,和二叔的小册子一样,redo log也是有固定大小的,比如配置了2GB,那么当redo log记录满时,也需要暂停,先把redo log中的数据往磁盘中更新,然后在从头开始记录新的更新操作。
如上图所示,可以看到redo log的数据结构是一个环状结构,checkpoint端点负责记录已经更新到磁盘的位点,write pos端点负责记录写入到redo log文件的位点,绿色区域表示redo log中空白的区域可以进行写入,红色区域表示redo log中已经写入的区域,等待着被checkpoint。
当checkpoint追上了write pos时,说明redo log中已经没有记录新操作的地方了,也就是全部是红色区域了。那么就需要停下来先将一部分数据更新到磁盘中,腾出一部分绿色区域来,然后在进行redo log的写入。
sql
# 创建表
create table geektao (a int, b int, primary key(a), key(b));
# 插入表
insert info geektao select 1,2;
# redo log 记录的内容
page(2,3), offset 32, value 1,2 #聚集索引
page(2,4), offset 64, value 2 #辅助索引
那么,redo log存储的内容是什么呢?redo log存储的是物理页的变更记录,需要注意的是redo log记录的是对物理页做了什么改动,并不是改动之后的物理页。如上所示,当我们向geektao表插入一条数据时,redo log中记录内容大致为这个样子,记录的是物理页的改动。就想二叔的小册子一样,只记录商品的售出记录,并不是商品的库存数。
二进制日志 binlog
binlog叫做二进制日志,也叫归档日志,上边说到的redo log是InnoDB独有的,属于存储引擎层的日志。而binlog是Mysql服务层的日志,那作为服务层的组件它的特点是什么呢?那就是所有的存储引擎共享。
redo log记录的物理页做了什么改动,binlog存储的内容是什么呢?binlog与redo log不同,binlog有两种模式,一开始只有statement格式,statement格式记录的是执行的sql语句,也就是你写的sql语句;后来有了row格式,row格式记录的是行的内容,为了做到数据的强一致性,row格式会把修改前后的记录都记下来,如果是insert语句,则会记录一条delete,如果是update语句,则会记录一条update之前的数据,这样当我们需要回滚时很方便就可以实现。
有些同学可以会问,既然已经有了redo log了,为什么还要有binlog呢?看起来两者实现的功能差不多啊。
Mysql在一开始默认的存储引擎是MySAM,但是MySAM是不支持事务的,而binlog作为服务层的日志也仅仅有归档的作用,说白了就是将你的修改简单做个备份,等Mysql服务突然崩了或者需要建新库时可以通过binlog记录回放某个表历史上所有的操作记录,但是binlog并不能保证强事务,也就是说存在物理页已经修改了,但是binlog还没写入就崩了,那么这个数据是无法恢复的。
InnoDB和MySAM不同,InnoDB是支持事务的,但是仅仅依靠服务层的binlog又无法满足。所以InnoDB只能在自己身上做文章。这才有了InnoDB独有的redo log日志。那有了redo log就可以实现事务了吗?还别说,真就实现了,下边我们说一说redo log和binlog是如何搭配达到实现事务的。
想了想,为了方便理解,这里还是先说一说二叔的故事:
二叔小卖铺的生意越来越好,一个人忙不过来了,于是二叔便招募了一个收银员,为了能够对账,二叔规定原来的小册子除了要记录卖了哪个商品卖了多少件,还得记录卖出这个商品收了多少钱。
二叔告诉收银员要这么做:
- 先在小册子中记录卖出了哪个商品,卖出多少件,收款多少钱;
- 然后将收到的钱放入钱柜里,确保收款和应收款是一样的;
- 接着再在小册子中记录这次交易的后边打个钩;
- 最后把商品给到顾客,交易完成。
晚上打烊之后,二叔就会拿着小册子和钱柜进行对账了,二叔是这样对的:
- 如果小册子中商品记录后边打了钩,就从钱柜里拿出对应的钱,代表本次交易有效;
- 如果小册子中商品记录后边没有打钩,钱柜里却有这部分钱,那就说明钱确实收到了,本次交易有效,那就在商品记录后边补个钩;
- 如果小册子中商品记录后边打了钩,钱柜里却没有这部分钱,那就说明钱没有收到。以钱柜为主,虽然打了钩,没收到钱说明没有卖出去。就将记录后边的钩子删掉,本次交易作废。
二叔为了能够对上账,做了两次确定操作,一次是确定商品记录是否打了钩,一次是确定钱柜里是否收到对应的钱,只有两次对应上了才是对账,只看商品记录不知道实际收了多少钱,只看钱柜不知道为什么收到了这些钱。
我们现在回过头来说一说redo log和binlog是如何保证事务的,相信习惯涛哥的同学已经差不多有点懂了。
因为redo log是存储引擎层的,binlog是服务层的,本来谁也管不找谁。但是要想保证两者之间数据一致性就只能通过某种规则使两者有序的保存各自的数据。而这个规则和二叔的对账类似,大致流程如下。还是用开头说的SQL举例,这里我在贴一遍:
sql
update geektao set name = 'geektao2' where id = 1;
- 连接器到执行器执行步骤略过...;
- 执行器调用存储引擎的修改接口,将id为1的name改为'geektao2';
- 存储引擎将这行数据更新到内存页中,同时将本次更新操作记录到redo log中,此时redo log处于prepare状态(在小册子中记录了但还没打钩),然后通知执行器修改完成,可以提交事务;
- 执行器收到存储引擎的通知,生成binlog并将binlog写入磁盘(将钱放入钱柜);
- 执行器调用存储引擎的提交事务接口,存储引擎把redo log改为commit状态,更新完成(在小册子记录后边打钩);
- 响应客户端操作结果影响行数。
假如某天Mysql服务器突然崩了需要进行恢复,上边提到如果只有binlog是无法将数据完全恢复成原来的样子的,如果加上redo log呢,大家可以对照着二叔对账的过程看:
- 如果某行数据变更在redo log中存在且为commit状态,在binlog中也存在,说明数据是一致的;
- 如果某行数据变更在redo log中存在且为prepare状态,在binlog中也存在,则提交本次事务,并将redo log状态修改为commit,数据一致;
- 如果某行数据变更在redo log中存在且为prepare状态,在binog中不存在,说明数据不一致了,事务并未提交,将本次记录回滚。
看和二叔记账是不是一样一样的,有同学可能会问,为什么这么麻烦,还要搞两个状态,记完redo log就代表commit了,在记录binlog不就行了。前边提到了,本身redo log和binlog各自负责各自的数据,没有什么联系,但是现在为了要保证一条修改数据在redo log和binlog之间是一致的,那就得叫两者有着联系,而两状态提交就是redo log和binlog之间的纽带。就像二叔对账一样,本来小册子只记录卖了啥,钱柜只管存钱,两者没有什么联系,那最后肯定得乱了套,所以打钩就是两者之间的纽带,打钩意味着钱入钱柜了,而钱柜里就应该有这笔钱,两者就取得了联系。
如果还有不懂的同学没有关系,后续有单独讲解Mysql中日志系统的篇章,那时候我会详细说一说,今天有个大概的理解就可以了。
小结
今天主要讲了Mysql中最重要的日志组件之二redo log和binlog,为什么是之二呢,因为还有之三😄。同样,今天也准备了一些问题帮助大家加深理解和记忆,大家可以尝试回答一下。
问题
- redo log是什么,有什么作用?
- binlog是什么,记录哪些内容?
- 为什么要用两阶段提交?
- redo log和binlog的区别是什么?
- Mysql服务宕机之后是如何恢复数据的?
- binlog设置多久备份一次,结合业务场景说一说?
- 如果误删或者误改了数据,如何恢复?
- WAL是什么,为什么会有WAL?