本文是 MySQL 实战 45 讲 (geekbang.org) 的学习笔记
请各位一定要支持作者大大,写的太棒了~
这里只是小付对文章学习的笔记~
貌似官网可以免费学习五小节哦~
我们在上一小结中学习了 一条 SQL 查询语句是如何执行的?
一条查询语句的执行过程一般是经过 连接器、查询缓存、分析器、优化器、执行器等功能模块,最后到达存储引擎。
注意点:MySQL 8.0 之后将查询缓存模块移除了哦~
那么一条 SQL 更新语句是如何执行流程又是如何的?
我们还是从一个表的一条更新语句说起,下面是这个表的创建语句,这个表有一个主键 ID
和一个整型字段 c
:
mysql> CREATE TABLE T (
ID int primary key ,
c int
);
如果此时要将 ID = 2 这一行的值加 1 , SQL 就会这么写
mysql> insert into t(id, c) values(2 , 0);
mysql> update t set c = c + 1 where ID = 2;
在上一小节中,我们了解了SQL 语句基本的执行链路。查询语句的那一套流程,更新语句也同样会走一遍。
查询和更新操作都要经过 Server 层 和 存储引擎层
回顾:当一个表上有更新的时候,此时跟这个表有关的查询缓存都会失效,所以这条更新操作的语句会把 T 表上所有查询缓存结果都清空,这也就是为什么我们一般不建议使用查询缓存的原因。
走过查询缓存,我们就会来到分析器,对我们传入的字符串进行词法分析 与 语法分析,通过分析解析知道当前传入的字符串是一条更新语句。
此时优化器就会工作了,它会去实现“如何做”——决定要使用 ID 这个索引。
然后,执行器 Just do it 具体执行,通过调用存储引擎提供的API接口找到这一行数据并且进行更新操作。
与查询流程不大一样的是——更新操作流程还囊括了两个重要的日志模块,它们正是我们今天要讨论的:redo log(重做日志)
和 bin log(归档日志)
。
DML:数据操纵语言(Data Manipulation Language, DML)—— 如 INSERT 、DELETE 、UPDATE、SELECT。
DDL:数据库模式定义语言(Data Definition Language,DDL)——如 CREATE 、 DROP 、ALTER等。
注意:其实SELECT实际上是归类于 DQL(数据库查询语言)但习惯上都将其归类于 DML。
举个
《孔乙己》这篇文章中,酒店掌柜有一个粉板,专门用来记录客人的赊账记录。如果赊账的人不多,那么他可以把顾客名和账目写在板上。但如果赊账的人多了,粉板总会有记不下的时候,这个时候掌柜一定还有一个专门记录赊账的账本。
如果有人要进行赊账 或者还账的话,掌柜一般有两种做法
很明显!在工作繁忙无暇顾及时,掌柜的一定会选择后面这种方法,因为不仅需要从 厚厚
的账本中找到对应人的记录,找到之后还需进行核算销账/记账 ,最后将此次记录写入修改等。
对照 ,这里 厚厚账本 找出一条记录,就好比是一个大麻烦,这个大麻烦实际指的是 随机 IO 。为了避免每次更新都需要通过磁盘随机 IO 定位到记录位置。将改动以 顺序 IO 的形式写到 redo log(重做日志)中,也就是写到这里的粉板上,可以使用组提交来进行批量更新,就好比打烊之后,闲置时间批量修改核算账本。在 Redis 中也有类似的如 pipeline 以减少 网络IO。批量请求。
对比之下:聪明的掌柜都会先在粉板上记一下,待到打烊后批量处理。
回到咱们的 MySQL ,在 MySQL 里也有这个问题,如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程 IO 成本、查找成本都很高为了解决这个问题,MySQL 的设计者就用了类似酒店掌柜粉板的思路来提升更新效率。
IO 成本主要来源于两大块时间:寻址时间以及上下文切换所需要的时间。
用户态和内核态的上下文切换。我们知道用户态是无法直接访问磁盘等硬件上的数据,只可以通过操作系统去调用内核态的接口,用内核态的线程去访问。
这里的上下文切换指的是?
这里的上下文切换指的是同进程的线程上下文切换,所谓上下文就是线程运行需要的环境信息。
首先:用户态线程需要一些中间计算结果保存在 CPU 寄存器,保存CPU指令的地址到程序计数器(执行顺序的保证),还需要保存栈的信息等一些线程私有的信息。
然后:由用户态切换到内核态的线程执行,此时就需要将线程的私有信息从寄存器,程序计数器里读出来,然后执行读磁盘上的数据。
读完后返回,又需要把执行完成后的线程信息写入寄存器和程序计数器,切换回用户态之后,用户态线程又要读之前保存的线程执行的环境信息,恢复执行。
这个过程主要是消耗时间资源。
出自:《Linux性能优化实战》——SQL 执行前优化器对SQL进行优化
WAL 的全称是 Write-Ahead Logging,它的关键点就是先写日志,再写磁盘,也就是先写粉板,等不忙的时候再写账本。
注意:“先写日志” 也是先写磁盘,知识写日志是顺序写盘,速度很快。
详细点:先写 redo log 到 log buffer ,具体内容就是针对哪个表空间的 哪些页面做了修改,然后 log buffer 中的日志内容会在某些时候写入到 redo log 日志文件中,比如事务提交时。
为什么写 redo log 会比 刷新内存中的数据页到磁盘快?
是因为服务器在启动时就已经给 redo 日志文件分配好了一块物理上连续的磁盘空间,每次写 redo 日志都是往文件中追加,并没有寻址的过程。
还是刚才那个例子
如果今天赊账的不多,掌柜可以等打烊后再整理。但如果某天赊账的特别多,粉板写满了,又怎么办呢?这个时候掌柜只好放下手中的活儿,把粉板中的一部分赊账记录更新到账本中,然后把这些记录从粉板上擦掉,为记新账腾出空间。
这就要说道 redo log 的写方式了
redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
大致如图所示
write Pos:是当前记录的位置,一边写一边向后移动,写到第 3 号文件末尾后就回到 0 号文件开头。
checkpoint: 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。
我们可以很清楚的看到 write pos 与 checkpoint 之间的是 “粉板” 空余部分空间,可以用来记录新的操作。如果 write pos 指针追上了 checkpoint 指针,此时便说明 “粉板” 满了,空间不足,不足以支持执行新的更新操作,需要停下来擦除掉一些记录,把checkpoint 推进一下。
❓ 问题1. 停下来擦掉记录,不会造成请求阻塞么?
❓问题2. 处理掉擦掉部分的时候还没有进行写入,如果此时 crash 那么 safe如何保障?
❓问题3. 如果光写 redo log 没有写入到数据库中,是否会导致数据一致性、实时性有问题?
❓问题4. 为什么要写到写不下才擦掉,队列处理错峰填谷不是更合理?
如何控制落盘机制
InnoDB
可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,具有了 crash - safe 能力(即使 MySQL 服务宕机,也不会丢失数据的能力)。crash - safe
要理解 crash-safe 这个概念,可以想想我们前面赊账记录的例子。只要赊账记录记在了粉板上或写在了账本上,之后即使掌柜忘记了,比如突然停业几天,恢复生意后依然可以通过账本和粉板上的数据明确赊账账目。
从MySQL 基本架构整体来看,MySQL 是分为两大板块的,其一就是 Server 层,它主要做的就是 MySQL 功能层面的事情 ,另一层就是存储引擎层,负责存储相关的具体事宜。刚刚我们介绍了 InnoDB 存储引擎所特有的日志模块 redo log ,而 Server 层也有属于自己的日志模块,称之为 binlog(归档日志)
在上一节中我们提到过,从 MySQL 5.5.5 之后才将 InnoDB 作为默认的存储引擎来创建数据库表,所以一开始MySQL 中是并没有 InnoDB 引擎的,MySQL 自带的存储引擎是 MyISAM ,但是 MyISAM 并不具备 crash - safe 的能力,binlog 日志也只能用于归档,其也不具备crash - safe能力,故 InnoDB 使用另外一套日志系统 redo log 来实现crash - safe 能力。
为什么 binlog 没有 crash - safe 能力 ?
不考虑mysql现有的实现,假如现在重新设计mysql,只用一个binlog是否可以实现cash_safe能力呢?
答案是可以的,只不过binlog中也要加入checkpoint,数据库故障重启后,binlog checkpoint之后的sql都重放一遍。但是这样做让binlog耦合的功能太多。
日志模块 | 不同点 |
---|---|
redo log | 1.InnoDB 引擎特有的 |2.redo log 是物理日志,记录的是“在某个数据页上做了什么修改”|3.redo log 是循环写的,空间固定会用完 |
binlog | 1.binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用|2.binlog 是逻辑日志,记录的是这个语句的原始逻辑,如“给 ID = 2 这一行 c 字段 加 1”|3.binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。 |
为什么必须有“两阶段提交”呢?
因为这是为了让两份日志之间的逻辑一致。要说明这个问题,我们得从文章开头的那个问题说起:
怎样让数据库恢复到半个月内任意一秒的状态?
binlog 会记录所有的逻辑操作(我这里认为这个逻辑操作就是相当于原始 SQL 语句),并且是采用“追加写”的形式。如果你的 DBA 承诺说半个月内可以恢复,那么备份系统中一定会保存最近半个月的所有 binlog,同时系统会定期做整库备份。这里的“定期”取决于系统的重要性,可以是一天一备,也可以是一周一备。
一个小场景
当需要恢复到指定的某一秒时,比如误删的数据库于 2022-04-11 14:00:00
发现表被误删,需要恢复到 2022-04-11 12:00:00
时刻的数据
2022-04-11 00:00:00
2022-04-11 00:00:00
开始从误删数据库中找出到误删前的增量binlog日志,并执行到临时库 这样就保证了临时库和误删前的库一致了。反证法论证
仍然用前面的 update 语句来做例子。假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?
可以看到,如果不使用“两阶段提交”,那么数据库的状态就有可能和用它的日志恢复出来的库的状态不一致
同时也是因为 redo log 负责事务; binlog负责归档恢复; 各司其职,相互配合,才提供(保证)了现有功能的完整性;
两阶段提交的场景不仅仅是恢复临时库的场景,还有对数据库扩容的业务场景
数据库的扩容,即增加备份库来提高系统读数据库的能力的时候,常采取全量备份+binlog实现。假如binlog和redo log记录的事务的逻辑状态不一致,则会导致严重的主从数据库数据不一致问题。
两阶段提交的根本目的就是为了让这两个日志状态保持逻辑上的一致
redo log 用于保证 crash-safe 能力。innodb_flush_log_at_trx_commit 这个参数设置成 1 的时候,表示每次事务的 redo log 都直接持久化到磁盘。这个参数我建议你设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。
sync_binlog 这个参数设置成 1 的时候,表示每次事务的 binlog 都持久化到磁盘。这个参数我也建议你设置成 1,这样可以保证 MySQL 异常重启之后 binlog 不丢失。
MySQL 日志系统密切相关的“两阶段提交”。两阶段提交是跨系统维持数据逻辑一致性时常用的一个方案
分布式系统也要用到 两阶段提交来保证事务
定期全量备份的周期“取决于系统重要性,有的是一天一备,有的是一周一备”。那么在什么场景下,一天一备会比一周一备更有优势呢?或者说,它影响了这个数据库系统的哪个指标?
首先,是恢复数据丢失的时间,既然需要恢复,肯定是数据丢失了。如果一天一备份的话,只要找到这天的全备,加入这天某段时间的binlog来恢复,如果一周一备份,假设是周一,而你要恢复的数据是周日某个时间点,那就,需要全备+周一到周日某个时间点的全部binlog用来恢复,时间相比前者需要增加很多;
看业务能忍受的程度 其次,是数据库丢失,如果一周一备份的话,需要确保整个一周的binlog都完好无损,否则将无法恢复;而一天一备,只要保证这天的binlog都完好无损;当然这个可以通过校验,或者冗余等技术来实现,相比之下,上面那点更重要。
优势点在于:好处是“最长恢复时间”更短。
影响的指标是:RTO(恢复目标时间 recovery time object)