最近有新闻说"丁奇"炒股失败欠债,赶紧去极客时间买了他的《MySQL 实战 45 讲》以防下架,顺带重新系统的复习下MYSQL相关知识,记录下学习笔记;
本篇介绍:
MySQL架构可以分为Server层和存储引擎层两部分:
(1)Server层,包括连接器、查询缓存、分析器、优化器、执行器等,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
(2)存储引擎层,负责数据的存储和提取;其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎;不同的存储引擎共用一个Server层;MySQL 5.5.5版本开始InnoDB成为了默认存储引擎。
1. 连接器,负责跟客户端建立连接、获取权限、维持和管理连接;一个建立连接后,这个连接里面的权限将依赖于此时读到的权限;权限修改后,只有再新建的连接才会使用新的权限设置。
建立连接的过程通常是比较复杂的,建议尽量使用长连接,客户端持续有请求,则一直使用同一个连接;但是,MySQL在执行过程中临时使用的内存是管理在连接对象里面的,在连接断开的时候才释放;
所以如果长连接累积下来,可能导致OOM被系统杀掉,从现象看就是 MySQL 异常重启了;因此建议:1.定期断开长连接;2.MySQL 5.7及以上版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源;
2. 查询缓存
MySQL 拿到一个查询请求后,会先查查询缓存;查询缓存中,key是查询的语句,value是查询结果;命中缓存直接返回,未命中则朝代结果后写入查询缓存;
不推荐使用查询缓存,原因:查询缓存失效频繁,只要有更新,这个表上所有的查询缓存都会被清空,因此不推荐使用;除非你的业务就是有一张静态表;MySQL 8.0 版本直接将查询缓存的整块功能删掉;
3. 分析器
对 SQL 语句做解析,包括词法解析和语法解析;
- 词法解析:如识别SQL语句中"select"关键字、把字符串“T”识别成“表名 T”、把字符串“ID”识别成“列 ID”等;
- 语法解析:语法错误,报错“You have an error in your SQL syntax”;如列不存在、表名不存在、关键字拼错了等;
4. 优化器
5. 执行器
开始执行语句;比如,你有个最简单的表,表里只有一个 ID 字段,假设ID 字段没有索引;
mysql> select * from T where ID=10;
执行前,先判断一下你对这个表 T 有没有执行查询的权限;查询会在优化器之前调用 precheck 验证权限;如果有权限,就打开表继续执行,执行器就会根据表的引擎定义,去调用存储引擎提供的接口,步骤如下:
(1)调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是 10,如果不是则跳过,如果是则将这行存在结果集中;
(2)调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行;
(3)执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端;
在有些场景下,执行器调用一次,在引擎内部则扫描了多行(引擎优化),因此引擎扫描行数跟 rows_examined 并不是完全相同的;
MySQL的架构包括:Server层和存储引擎层;
Server层——各存储引擎共用
连接器:建立连接、权限管理;
查询缓存:K为SQL,V为返回结果;鸡肋,表更新时清除,MySQL8.0版本已移除;
分析器:语法分析(验证SQL语法正确)和词法分析(解析SQL);
优化器:索引选择和联表顺序选择;
执行器:调用存储引擎读写接口,返回数据给客户端;
存储引擎——插件式,提供读写接口,MySQL5.5.5之后默认使用Innodb存储引擎;
redo log(重做日志) 是 InnoDB 引擎特有的日志,而 Server 层也有自己的日志,称为 binlog(归档日志);
1. redo log(为什么用redo log)
如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录,然后再更新,整个过程IO成本、查找成本都很高;MySQL使用 WAL (Write-Ahead Logging) 技术,先写日志,再写磁盘,类比记账时先写粉板,等不忙的时候再写账本;
粉板写满了,又怎么办呢?InnoDB的redo log是固定大小的;如图,可以配置为一组 4 个文件,每个文件的大小是 1GB,那么这块“粉板”总共就可以记录 4GB 的操作;从头开始写,写到末尾就又回到开头循环写,write pos 和 checkpoint 之间的是“粉板”上还空着的部分,如果 write pos 追上 checkpoint,表示“粉板”满了,这时候不能再执行新的更新,得停下来先执行一部分操作后再擦这些记录,把 checkpoint 推进一下;
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe;因为数据已经写入"粉板" redo log;
2. binlog
1. redolog只有InnoDB有,别的引擎没有;
2. redolog是循环写的,历史已执行的会被覆盖,不全量持久保存,因此binlog的“归档”这个功能,redolog是不具备的;
1. redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 层实现的,所有引擎都可以使用;
2. redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如“给 ID=2 这一行的 c 字段加 1 ”;
3. redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的,即文件写到一定大小后会切换到下一个文件,历史日志不会被覆盖;
先看[执行器](绿色)与[InnoDB引擎](红色)是如何更新一条指定的数据的:
可见,redo log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交";下面思考下关于"两阶段提交"的几个问题:
结论:在于崩溃恢复crash-safe;
MySQL为了提升性能,引入了BufferPool缓冲池,来优化CPU与磁盘之间的鸿沟,即——Innodb以数据页Page为单位来管理存储空间,为避免每次修改Page页都需要落到磁盘,导致频繁的IO,从而影响CPU命令的执行效率,MYSQL引入BufferPool缓冲池,先刷页的数据到缓冲池,在缓冲池进行一系列数据变更,而后统一定期刷入磁盘,从而避免了频繁的IO;
查询数据时,先从BufferPool中查询,查询不到则从磁盘加载数据页到BufferPool;每次对数据的更新,也不总是实时刷新到磁盘,而是先同步到BufferPool中,涉及到的数据页就会临时变成脏页;同时会启动后台线程,异步地将脏页刷新到磁盘中,来完成BufferPool与磁盘的数据同步;
如果在某个时间,MySQL突然崩溃,则内存中的BufferPool就会丢失,剩余未同步的数据就会直接消失;虽然在更新BufferPool后,也写入了binlog中,但binlog并不具备crash-safe能力,而redolog具备crash-safe能力;因此需要redolog;
跟这两种日志的特点有关:
redolog和binlog有一个很大的区别就是,一个是循环写(物理日志,记录数据页上的修改),一个是追加写(逻辑日志,记录数据的变更逻辑);也就是说redolog只会记录未刷盘的日志,已经刷入磁盘的数据都会从redolog这个有限大小的日志文件里删除;而binlog是追加日志,保存的是全量的日志;
当数据库crash后,想要恢复未刷盘但已经写入redolog和binlog的数据到内存时,仅凭借binlog是无法恢复的,原因——虽然binlog拥有全量的日志,但没有一个标志让innoDB判断哪些数据已经刷盘,哪些数据还没有;而redolog的特性就天然的具备这个标志——已经刷入磁盘的数据都会从redolog删除(checkpoint移动);
为什么不给binlog也搞一个状态位呢?——因为最终是存储引擎与磁盘IO交互的是Innodb存储引擎,而非MySQL的Server层直接实现;binlog是MySQL的Server层实现的,所有引擎都可以使用,而redolog是InnoDB引擎特有的;因此binlog没办法知晓缓存何时真正落入磁盘,而redolog却可以知晓,因此binlog没有类似redolog的两阶段的状态;
此外,binlog是可以通过配置项关闭的,只依赖binlog来恢复数据靠不住;
前提:
a. 由于redolog和binlog分别由存储引擎和MySQL Server层实现的,是两个独立的逻辑,因此一定存在先后关系;
b. MySQL的主从同步就是根据binlog来的,从库会执行在主库已经写入binlog的操作,从而与主库数据保持一致(存在短暂主从延迟);
假设:
I. 先写redolog再写binlog;
当前事务提交后,写入redo log成功,之后主节点崩溃(写bin log之前);在主节点重启后,主节点利用redo log进行恢复;但由于bin log没有这条数据,就会导致主节点相对于从节点多出来一条数据,造成主从数据不一致;
II. 先写binlog再写redolog;
当前事务提交后,写入bin log成功,之后主节点崩溃(在写redo log之前);那么在主节点重启后,由于没有写入redo log,因此不会恢复该条数据到磁盘;但从节点会依据bin log在本地回放,因此会相对于主节点多出来一条"在主节点实际未刷入磁盘"的数据,从而产生主从不一致;
综上所述,只写一次redo log与bin log,无法保证这两种日志在事务提交后的一致性,实际上也就是无法保证主节点崩溃恢复与从节点本地回放数据的一致性;因此,需要写2次redo log;
前提:首先比较重要的一点是,在写入redolog时,会顺便记录XID,即当前事务id;在写入binlog时,也会写入XID;
- 如果在写入redolog之前崩溃,那么此时redolog与binlog中都没有,是一致的情况,崩溃也无所谓;
- 如果在写入redolog prepare阶段后立马崩溃,之后会在崩恢复时,由于redolog没有被标记为commit,于是拿着redolog中的XID去binlog中查找,此时肯定是找不到的,那么执行回滚操作;
- 如果在写入binlog后立马崩溃,在恢复时,由redolog中的XID可以找到对应的binlog,这个时候直接提交即可;
总的来说,在崩溃恢复后,只要redolog不是处于commit阶段,那么就拿着redolog中的XID去binlog中寻找,找得到就提交,否则就回滚;
在这样的机制和规则下,两阶段提交能在崩溃恢复时,能够对提交中断的事务进行补偿,来确保redolog与binlog的数据一致性;
1. 为什么用redolog?(作用/好处)
- 避免每次更新都要访问磁盘找到记录再更新,从而避免对磁盘频繁的随机IO,MySQL使用WAL技术先写日志再写磁盘;
- redolog具备crash-safe崩溃恢复能力:(1)是物理日志,可以直接作用于内存页,用于快速恢复内存数据;(2)仅保留未落盘的记录日志,从而知道从何处开始恢复数据;
2. binlog与redolog区别?
- 1.redolog是InnoDB特有的;binlog是MySQL的Server层实现的,所有引擎都可以使用;
- 2.redolog是物理日志,记录的是“在某个数据页上做了什么修改”;binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1”;
- 3.redolog是循环写的,数据落盘则不再保留;binlog能一直写,历史日志不会被覆盖;
3. 什么是两阶段提交?
- 更新数据时,redolog的写入拆成了两个步骤:redolog先标记prepare(中间写binlog),最后redolog再commit,这就是"两阶段提交";
4. 为什么需要两阶段提交?(为什么要写2次redolog?)
- 若只提交一次redolog是不行的:(1)写入binlog成功,之后主节点崩溃(在写redolog之前),则导致备库数据多于主库;(2)写入redolog成功,之后主节点崩溃(写binlog之前),导致主库数据根据redolog恢复后,备库数据落后于主库;
- redolog的两阶段提交方案,结合崩溃恢复的规则,就可以保证数据的恢复和主备数据的一致性;
5. 在两阶段提交下,是怎么实现崩溃恢复的呢?
- 在崩溃恢复后,只要redolog不是处于commit阶段,那么就拿着redolog中的XID去找对应的binlog,找得到对应的binlog就提交事务,否则就回滚事务;在这样的规则下,两阶段提交能在崩溃恢复时,能够对提交中断的事务进行补偿,保证主备数据一致性;
参考:
为什么 redo log 具有 crash-safe 的能力,是 binlog 无法替代的?
事务的经典例子就是转账场景,A扣款成功和B收款成功,要么都成功,要么都失败;简单来说,事务就是要保证一组数据库操作,要么全部成功,要么全部失败;
在MySQL中,事务支持是在引擎层实现的;MySQL是一个支持多引擎的系统,但并不是所有的引擎都支持事务;比如MySQL原生的MyISAM引擎就不支持事务,这也是MyISAM被InnoDB取代的重要原因之一;
ACID(Atomicity、Consistency、Isolation、Durability),即原子性、一致性、隔离性、持久性;
当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念;
- 脏读:读到其它事务未提交的数据;("读未提交"隔离级别会发生)
- 不可重复读:同一个事务内前后读取的记录内容不一致,其他事务提交了更新,当前事务重新读会读到最新提交的数据;("读提交"隔离级别会发生)不能说"不可重复读"就一定存在问题,还是要看应用场景;
- 幻读:跟不可重复读类似,只不过其他事务对数据的更新换成了插入/删除操作,导致同一个事务内前后读取的记录数量不一致;("读提交"隔离级别会发生)
- 读未提交(read uncommitted):一个事务还没提交时,它做的变更就能被别的事务看到;
- 读已提交(read committed):一个事务提交之后,它做的变更才会被其他事务看到;
- 可重复读(repeatable read):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据(视图,可以理解成那一刻数据的快照)是一致的;当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的;
- 串行化(serializable):对于同一行记录,“写”会加“写锁”,“读”会加“读锁”;当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;
这4种隔离级别,并行性能依次降低,并发时的安全性依次提高,所以需要做权衡;举个例子说明隔离级别:
我们来看看在不同的隔离级别下,事务 A 会有哪些不同的返回结果,也就是图里面 V1、V2、V3 的返回值分别是什么;
- 若隔离级别是“读未提交”, 则 V1 的值就是 2;这时候事务 B 虽然还没有提交,但是结果已经被 A 看到了;因此,V2、V3 也都是 2;
- 若隔离级别是“读提交”,则 V1 是 1,V2 的值是 2;事务 B 的更新在提交后才能被 A 看到;所以, V3 的值也是 2;
- 若隔离级别是“可重复读”,则 V1、V2 是 1,V3 是 2;之所以 V2 还是 1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的;
- 若隔离级别是“串行化”,则在事务 B 执行“将 1 改成 2”的时候,会被锁住;直到事务 A 提交后,事务 B 才可以继续执行;所以从 A 的角度看, V1、V2 值是 1,V3 的值是 2;
在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准;
- 在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图;
- 在“读提交”隔离级别下,这个视图是在每个SQL语句开始执行的时候创建的;
- 在“读未提交”隔离级别下,直接返回记录上的最新值,没有视图概念;
- 在“串行化”隔离级别下,直接用加锁的方式来避免并行访问;
MySQL默认的隔离级别是"可重复度",以"可重复度"来展开说明事务隔离具体是怎么实现的;
在MySQL中,每条记录在更新的时候都会同时记录一条回滚操作(记录undo log);记录上的最新值,通过回滚操作,都可以得到前一个状态的值;
假设一个值从1被按顺序依次改成了2、3、4,在回滚日志里面就会有类似下面的记录;
由图可知:
- 当前值是4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的视图read-view;在视图A、B、C里面,这一个记录的值分别是1、2、4;
- 同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC);
- 对于视图A,要得到1,就必须将当前值依次执行图中所有的回滚操作得到;
回滚日志(undo log)会在什么时候删除 ?
回滚日志是用来将当前值依次恢复成视图中的值(数据在事务开启时的快照),当没有事务再需要用到这些回滚日志时(即),回滚日志会被删除;也就是说,当没有比回滚日志更早的读视图(读视图在事务开启时创建)的时候,这个回滚日志也就失去了用武之地,就可以删除了;
长事务的从开启到提交的时间相对更久、涉及的数据范围相对更广;由于其他的事务一直在持续执行和提交,因此系统里面会存在很老的这个长事务的视图;所以这个事务提交之前,数据库里面它可能用到的回滚记录undo log都必须保留,这就会导致大量占用存储空间;此外,长事务还占用锁资源,也可能拖垮整个库;
如何查找长事务呢?
可以在 information_schema 库的 innodb_trx 这个表中查询长事务,比如下面这个语句,用于查找持续时间超过 60s 的事务:
- select * from information_schema.innodb_trx
- where TIME_TO_SEC(timediff(now(), trx_started)) > 60;
如何避免长事务对业务的影响?
- 尽量避免使用了set autocommit=0,如果用到了,事务提交后需要把它改回1;
- 确认是否有不必要的只读事务;有些框架会习惯不管什么语句先用begin/commit框起来,导致把好几个select语句放到了事务中,这种只读事务可以去掉;
- 业务连接数据库的时候,根据业务本身的预估,通过SETMAX_EXECUTION_TIME命令,来控制每个语句执行的最长时间,避免单个语句意外执行太长时间;
- 监控information_schema.Innodb_trx表,设置长事务阈值,超过就报警/或者kill;
- 在业务功能测试阶段分析general_log,尽量早提前发现问题;
1. 什么是"事务"?事务的4大特性?
- 事务就是一组SQL命令,要么全部成功,要么全部失败;
- ACID是事务4大特性,即原子性(要么全成功要么全失败)、一致性(事务执行前后,数据总是正确的)、隔离性(并发的事务对彼此事务内数据的可见性)、持久性(事务提交后则数据会持久化);
2. 事务的4个隔离级别?
- 读未提交:事务还未提交,它的变更就能被其他事务看到;
- 读已提交:事务提交之后,它的变更才能被其他事务看到;
- 可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的;
- 串行化:对于同一行记录,“写”会加“写锁”,“读”会加“读锁”,读写和会阻塞;
3. 不同级别下会导致的问题?
- 脏读:读到其它事务未提交的数据;("读未提交"隔离级别会发生)
- 不可重复读:同一个事务内前后读取的记录内容不一致,当前事务执行过程中若其他事务提交了更新,当前事务重新读会读到最新提交的数据;("读提交"隔离级别会发生)
- 幻读:跟不可重复读类似,只不过其他事务对数据的更新换成了插入/删除操作,仅表示同一个事务内前后读取的记录数量不一致;("读提交"隔离级别会发生)
4. "可重复度"隔离级别是怎么实现的?(什么是MVCC?什么是undolog?)
- MySQL让同一条记录在系统中可以存在多个版本(也就是数据库的多版本并发控制MVCC),版本与事务ID一一对应,让不同事务看到自己版本的数据,从而实现"可重复度";
- "多个版本"并非真的存在,而是通过undolog回滚日志作用于最新的数据版本而算出来的;
5. 什么是"长事务"?有什么危害?
- 长事务就是执行时间久、涉及的数据范围广的事务;
- 长事务执行期间,其他的事务一直在持续提交,这就导致所涉及的数据要保留多个版本,即需要保留大量的undo log大量占用存储空间;此外,长事务还占用锁资源,也可能拖垮整个库;
下篇文章:《MySQL实战45讲》——学习笔记04-05 “深入浅出索引、最左前缀原则、索引下推优化“
本文学习资料:
01 | 基础架构:一条SQL查询语句是如何执行的?-极客时间