MySQL 架构|给你一个“上帝视角”
“我平时的工作就是 CRUD (增删改查)呀!我怎么提升自己的技术?“、”平时开发我都是用开源的 MyBatis、Hibernate,连原生的 sql 我都没写过几行”、“SQL 调优除了加索引,我也没有什么特别的手段去优化了”。
相信这样的问题你可能也会遇到,但是,这种低水平重复工作做多了后,不免要担心一点:我们要如何才能打破这种僵局,进一步提升自己的技术呢?
从本节起,我们先从与 CRUD 密不可分的数据库开始讲起。MySQL 相信你并不陌生,它已成为多年来最流行的开源数据库。所以,我们将针对 MySQL 展开数据库领域的进阶提升,不再只满足于写简单的 SQL。
本节我们就来 MySQL 的基础架构。
逻辑架构图——“千呼万唤始出来”
首先,让我们来一起看一起 MySQL 的逻辑架构图,如下:
从图中可以看出,MySQL 可以分为「Server 层」和「存储引擎层」。
为了更好地理解其中的细节,我们来看一下更加具体的逻辑架构。
注:该图片来自于网络,后面文章中如果已有不错的图片,我就不再重复“造轮子”。图侵删。
结合图片,我们简单解释一下各部分的作用:
-
最上层:客户端。大多数基于网络的客户端/服务端服务都有类似的架构。
如连接处理、授权认证、安全等。
-
第二层:核心服务功能。对应图中「连接器分析器、优化器、执行器、查询缓存」。
包含内置函数(如日期、时间、数学函数等)、跨存储引擎功能如存储过程、触发器、视图等。
-
第三层:存储引擎。负责数据的存储和提取。其架构是插件式的,支持 InnoDB、MyISAM、Memory等多个存储引擎。
具体实现:服务器通过 API 与存储引擎通信,不同存储引擎不会互相通信,只是简单响应服务器请求。
从 MySQL 5.5.5 开始,InnoDB 成为了默认的存储引擎。
这里我想补充一点,MySQL 最重要的特性就是「存储引擎架构」。
它的优点非常突出,可以将查询处理及其他系统任务和数据的存储/提取相分离,即「处理和存储分离设计」。
- 启发一: plugin 这种可插拔式的设计给予了 MySQL 存储引擎非常大的灵活性。
- 启发二:存储与计算分离也是当下热门的趋势,如 Apache Pulsar。
一条 SQL 的“奇幻漂流”
接下来,我们通过一条 SQL,跟随着它的“奇幻漂流”,来逐一认识一下看看架构图中的不同部分。
为了有个直观的感觉,我们就假设要执行的 SQL 为: select * from T where id=1;
。
连接器
第一步,我们要先连接到这个数据库上。
连接器的主要作用:负责和客户端建立连接、获取权限、维持和管理连接。
常见的命令一般为:
mysql -h$ip -P$port -u$user -p
输完命令之后,你就需要在交互对话里面输入密码。
这里我想提醒一下:如果用户名密码认证通过,连接器会去权限表中查询用户的所有权限。之后,这个连接的权限都依赖于此时读到的权限。也就是说,即使中途用户权限变更,也不会影响已经存在连接的权限。(只有新建的连接才使用新权限)
show processlist
连接完成后,如果没有后续操作,这个连接就处于空闲状态。可以用 show processlist
命令中看到它。下图就是执行结果,其中 Command 列显示为“Sleep”的这一行,就表示当前系统里有一个空闲连接。
如果客户端太长时间没有动静,连接器就会自动断开连接。这个时间是由参数wait_timeout
控制的,默认值是8小时。
如果你之前应用中用到了 MySQL,可能就会遇到过在日志中有时空闲太长,应用与 MySQL 的连接断开的情况,这就是根源所在。
连接被断开之后,客户端再次发送请求,就会收到一个错误提醒: Lost connection to MySQL server during query。如果你要继续请求,就需要重连。
「长连接」和「短连接」
-
长连接:连接成功后,如果客户端持续请求,一直使用同一个连接。
-
短连接:每次执行完很少的几次查询就断开连接,下次查询再新建一个。
因为建立连接的过程比较复杂,建议你尽量使用长连接。
但全部使用长连接后,你可能会发现,有时 MySQL 占用内存涨得特别快,因为MySQL在执行过程中临时使用的内存是管理在连接对象里面的。这些资源只有在连接断开时才会释放。所以如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(OOM),从现象看就是 MySQL 异常重启。
怎么解决这个问题呢?你可以考虑以下两种方案:
-
定期断开长连接。
使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。
-
如果你用的是MySQL 5.7或更新版本,可以在每次执行一个比较大的操作后,执行
mysql_reset_connection
重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚创建完时的状态。
查询缓存
上一步我们完成连接建立后,执行逻辑就来到了第二步:查询缓存。
与我们在应用端使用缓存(如 HashMap/redis 等)类似,MySQL 拿到一个查询请求后,会先去查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果会以 key-value 对的形式,直接缓存在内存中。
key 是查询的语句,value 是查询的结果。
- 如果命中,就直接返回 value 结果。
- 如果未命中,继续后面的流程,执行完后,将未命中的结果放入缓存中。
你看,这个过程和我们平时在应用开发中实现的思路是不是非常类似?我们在学习 MySQL 同时也能学习到缓存的实现方式。
尽管缓存可以提高查询效率,但是,大多数情况下建议不要使用查询缓存。
因为查询缓存往往弊 >> 利。简言之,失效频繁,对更新较多的情况下,缓存命中率非常低。
查询缓存的失效非常频繁,只要对一个表更新,则该表上所有的查询缓存都会被清空。因此很可能你费劲地把结果缓存起来,还没来得及使用呢,就被一个更新全清空了。
对于更新压力大的数据库来说,查询缓存命中率非常低。除非你的业务就是有张静态表,很长时间才会更新一次。比如系统配置表,那才适合使用查询缓存。
适用的场景:静态表或者变更频率非常低的表。
这里扩展一下,对于你确定要使用查询缓存的语句,可以用SQL_CACHE显式指定,像下面这个语句一样:
mysql> select SQL_CACHE * from T where id=10;
⚠️ MySQL 8.0 直接将查询缓存的整块功能删掉了,没有这个功能。
分析器
接下来,就要对 SQL 进行解析了。
首先,先做「词法分析」。
SQL语句是由多个字符串和空格组成的,MySQL需要识别出里面的字符串分别是什么,代表什么。
例如,MySQL 从你输入的"select"这个关键字识别出来,这是一个查询语句。它也要把字符串“T”识别成“表名T”,把字符串“id”识别成“列id”。
然后做「语法分析」。
语法分析会根据语法规则,判断 SQL 语句是否满足 MySQL 语法。
如果你的语句不对,就会收到“You have an error in your SQL syntax”的错误提醒,例如这个语句select少打了开头的字母“s”。
一般语法错误会提示第一个出现错误的位置,所以你要关注紧接“use near”的内容。
优化器
经过了分析器,接下来便是优化器的处理。
优化器在表里有多个索引的时候,决定使用哪个索引;或者在一个语句多表关联(join)的时候,决定各个表的连接顺序。
举个例子,
比如你执行下面这样的语句,这个语句是执行两个表的join:
mysql> select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20;
- 既可以先从表t1里面取出c=10的记录的ID值,再根据ID值关联到表t2,再判断t2里面d的值是否等于20。
- 也可以先从表t2里面取出d=20的记录的ID值,再根据ID值关联到t1,再判断t1里面c的值是否等于10。
两种执行方法的逻辑结果一样的,但执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。
类似的,还有大表驱动小表等。你写的 sql 语句的顺序不一定是MySQL 真正执行的顺序,原因就在这里。
执行器
前面我们通过分析器知道要做什么,通过优化器知道怎么做,然后就进入了执行器,开始执行 SQL。
1.开始执行前,先做权限验证。
先判断一下你对这个表T有没有执行权限,如果没有,就会返回没有权限的错误(在实现上,如果命中查询缓存,会在查询缓存放回结果的时候,做权限验证。查询也会在优化器之前调用 precheck 验证权限)。
2.如果有权限,就打开表继续执行。执行器会根据表的引擎定义,选择用这个引擎的接口。
比如这个例子中的表T中,ID字段没有索引,则执行器的执行流程是这样的:
- 调用 InnoDB 引擎接口取这个表的第一行,判断ID值是不是 10,如果不是则跳过,如果是则将这行存在「结果集」中;
- 调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
- 执行器将遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。
3.至此,语句执行完成。
如果表有索引,执行逻辑差不多。第一次调用的是“取满足条件的第一行”这个接口,之后循环取“满足条件的下一行”这个接口,这些接口都是引擎中已经定义好的。
这里补充一下:我们在数据库的慢查询日志中会看到一个 rows_examined
的字段,表示某个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行时累加的。
到这里,我们的 SQL "奇幻漂流"也画上一个句号。