数据库分层结构和存储引擎

一、数据库分层结构

MySQL逻辑结构整体分为三层:客户端层、服务层、存储引擎层。

最上层为客户端层,并非MySQL所独有,诸如:连接处理、授权认证、安全等功能均在这一层处理。主要负责与客户端进行交互,建立连接,记住连接状态,返回数据,响应请求,这一层是和客户端打交道的。

中间层为服务层,MySQL大多数核心服务均在中间这一层,包括查询解析、分析、优化、缓存、内置函数(比如:时间、数学、加密等函数)。所有的跨存储引擎的功能也在这一层实现:存储过程、触发器、视图等。主要负责查询处理、事务管理等其他数据库功能处理。

最下层为存储引擎,其负责MySQL中的数据存储和提取。实际物理磁盘上存储的文件,主要分为数据文件、日志文件。和Linux下的文件系统类似,每种存储引擎都有其优势和劣势。中间的服务层通过API与存储引擎通信,这些API接口屏蔽了不同存储引擎间的差异。

从图中不难看出,不同的存储引擎共用一个Server层,也就是从连接器到执行器的部分。存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。现在最常用的存储引擎是InnoDB,它从MySQL 5.5.5版本开始成为了默认存储引擎。这说明create table建表的时候,如果不指定引擎类型,默认使用的就是InnoDB。当然也可以指定存储引擎,例如create table语句中使用engine=memory, 来指定使用内存引擎创建表。接下来我们一个一个看各个组件的各自作用以及一条sql在整个架构的执行流程。

以查询为例:

首先接受到查询sql之后,数据库会立即分配一个线程对其进行处理;第一步查询处理器会对SQL查询进行优化,优化后会生成执行计划;然后交由计划执行器来执行。计划执行器需要访问更底层的事务管理器、存储管理器来操作数据,他们各自的分工各有不同;最终通过调用物理层的文件获取到查询结果信息,将最终结果响应给应用层。

1、连接器

当我们要执行select * from T where ID=1;这条语句的时候,首先当然是连接器帮我们负责跟客户端建立连接,获取权限、位置和管理连接。连接命令如下:

mysql -h$ip -P$port -u$user -p

输完命令之后,接下来就是经典的TCP握手了,连接器就要开始认证你的身份,这个时候用的就是你输入的用户名和密码。虽然密码也可以直接跟在-p后面写在命令行中,但这样可能会导致你的密码泄露。如果你连的是生产服务器,千万不要这么做,这是生产上的禁忌。如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。这就意味着,一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。

如果连接完成后,在未来的一段时间里没做任何操作,这个连接就处于空闲的状态,可以通过show processlist命令查看,如下所示:

客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由配置文件my.ini里的参数wait_timeout控制的,默认值是8小时。

如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒: Lost connection to MySQL server during query。这时候如果你要继续,就需要重连,然后再执行请求了。 

数据库建立连接的过程通常是比较复杂的,使用中尽量减少连接的动作,也就是尽量使用长连接。因为长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接。短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个,这样造成开销很大。

但是你会发现全部使用长连接后,有些时候MySql占用的内存会飙涨的很快。这是由于MySql在执行的过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时候才释放。所以如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(OOM),从现象看就是MySql异常重启了。

那么如何解决这种现象呢?主要有两种方案

    1.定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。

    2.如果你使用的版本是mysql 5.7以后的版本,可以在执行一个较大的操作后,通过执行mysql_reset_connection来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。

MySQL客户端/服务端通信协议是“半双工”的:在任一时刻,要么是服务器向客户端发送数据,要么是客户端向服务器发送数据,这两个动作不能同时发生。一旦一端开始发送消息,另一端要接收完整个消息才能响应它,所以我们无法也无须将一个消息切成小块独立发送,也没有办法进行流量控制。

客户端用一个单独的数据包将查询请求发送给服务器,所以当查询语句很长的时候,需要设置max_allowed_packet参数。但是需要注意的是,如果查询实在是太大,服务端会拒绝接收更多数据并抛出异常。

与之相反的是,服务器响应给用户的数据通常会很多,由多个数据包组成。但是当服务器响应客户端请求时,客户端必须完整的接收整个返回结果,而不能简单的只取前面几条结果,然后让服务器停止发送。因而在实际开发中,尽量保持查询简单且只返回必需的数据,减小通信间数据包的大小和数量是一个非常好的习惯,这也是查询中尽量避免使用SELECT *以及加上LIMIT限制的原因之一。

2、查询缓存

  连接建立完成后,就可以执行select语句去查询了,这时候执行逻辑就走到第二步:查询缓存。MYSQL拿到一个请求的时候,会先去缓存看有没有这个这条语句的执行结果,之前执行过的语句以及结果会以key-value 的形式缓存在内存中,当然,key就是sql语句了,value 就是之前的执行结果。如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你可以看到,如果查询命中缓存,MySQL不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高。

但是大多数情况下,强烈不建议你去使用查询缓存,这时候你们肯定会想,为什么不用呀,这不是挺好的呀?

原因一: cache 的访问由一个单一的全局锁来控制,这时候大量的查询将被阻塞,直至锁释放。所以不要简单认为设置 cache 必定会带来性能提升。

原因二:这是因为只要有对一个表的更新,这个表上所有的查询缓存都会被清空。这时候就会造成查询缓存的失效非常频繁,你费了很大劲地把结果存起来,还没使用呢,就被一个更新全清空了。对于更新压力大的数据库来说,查询缓存的命中率会非常低。除非你的业务就是有一张静态表,很长时间才会更新一次。比如,一个系统配置表,那这张表上的查询才适合使用查询缓存。

mysql还是很人性化的,根据需要去使用查询缓存,可以将参数query_cache_type设置成DEMAND,这样对于默认的SQL语句都不使用查询缓存。而对于你确定要使用查询缓存的语句,可以用SQL_CACHE显式指定,sql例子如下所示:

mysql> select SQL_CACHE * from T where ID=10;

最近我去官网看了mysql 8.0的改变,这个查询功能整块被删掉了,也就是8.0以后的版本都没有这个功能了。

3、分析器

如果没有命中查询缓存,就要开始真正执行语句了。首先,MySQL通过关键字将SQL语句进行解析,分析器先进行词法分析,mysql需要识别出你这条sql语句字符串里面的字符串分别是什么,代表什么意思,生成一颗对应的解析树。比如,mysql会根据你输入的select这关键字识别出来,这是一个查询语句,把“T”识别成表名T,把ID识别成字段ID。接着就是进行语法分析了,根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个SQL语句是否满足MySQL语法。比如检查要查询的数据表和数据列是否存在等等。如果你的语法错误,就会报出如下错误:

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'elect * from t where ID=1' at line 1

一般语法错误会提示第一个出现错误的位置,所以关注的是紧接“use near”的内容。 

4、优化器

MySQL使用基于成本的优化器,同一个sql语句可能会产生多个执行计划,优化器选择最优的执行计划交给执行器去执行。

经过了分析器后,在执行之前,还需要经过优化器的处理,为什么还需优化器呢?因为优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。比如你执行下面这样的语句,这个语句是执行两个表的join:

mysql> select * from T1 join T2 using(ID)  where T1.A=1 and T2.B=2;

这条语句既可以先从表T1里面取出A=1的记录的ID值,再根据ID值关联到表T2,再判断T2里面B的值是否等于2。也可以先从表T2里面取出B=2的记录的ID值,再根据ID值关联到T1,再判断T1里面A的值是否等于1。虽然最终执行的结果是一样的,但是执行效率却有很大的不同。再比如优化器是怎么选择索引的,例子如下:

SELECT C FROM T WHERE  A= 'value1' AND B = 'value2';

假设A上的扫描了100个数据行,B上扫描50个数据行,而同时进行的测试只得到了30个数据行。

先根据A会有100个数据行,接着进行匹配找到其中的30个与B中的值匹配记录,其中就有70次是失败了。

先根据 B会有50个数据行,接着进行匹配找到其中的30个与A中的值匹配的记录,只有20次是失败的,很显然需要的计算和磁盘I/O更少。

其结果是,优化器会先选择B索引,因为这样做开销更小。而优化器的作用就是决定选择使用哪一个方案。

因此MySQL 的优化器主要干如下几个重要的事情:

1、选择最合适的索引;
2、选择表扫还是走索引;
3、选择表关联顺序;
4、优化 where 子句;
5、排除管理中无用表;
6、决定 order by 和 group by 是否走索引;
7、尝试使用 inner join 替换 outer join;
8、简化子查询,决定结果缓存;
9、合并试图;

有非常多的原因会导致MySQL选择错误的执行计划,比如统计信息不准确、不会考虑不受其控制的操作成本(用户自定义函数、存储过程)、MySQL认为的最优跟我们想的不一样(我们希望执行时间尽可能短,但MySQL只选择它认为成本小的,但成本小并不意味着执行时间短)等等。

5、执行器

在完成解析和优化阶段以后,MySQL会生成对应的执行计划,查询执行引擎根据执行计划给出的指令逐步执行得出结果。整个执行过程的大部分操作均是通过调用存储引擎实现的接口来完成,这些接口被称为handler API。查询过程中的每一张表由一个handler实例表示。实际上,MySQL在查询优化阶段就为每一张表创建了一个handler实例,优化器可以根据这些实例的接口来获取表的相关信息,包括表的所有列名、索引统计信息等。存储引擎接口提供了非常丰富的功能,但其底层仅有几十个接口,这些接口像搭积木一样完成了一次查询的大部分操作。

经过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。开始执行的时候,要先判断一下你对这个表T有没有执行查询的权限,如果没有,就会返回没有权限的错误,如下所示。

select * from T where ID=1;

ERROR 1142 (42000): SELECT command denied to user 'b'@'localhost' for table 'T'

如果有权限,就继续往下执行,这时候执行器就会根据表的引擎定义,去使用这个引擎提供的接口。这条语句在执行器的执行流程如下:调用InnoDB引擎接口取这个表的第一行,判断ID值是不是1,如果不是则跳过,如果是则将这行存在结果集中;调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。

查询执行的最后一个阶段就是将结果返回给客户端。即使查询不到数据,MySQL仍然会返回这个查询的相关信息,比如该查询影响到的行数以及执行时间等等。如果查询缓存被打开且这个查询可以被缓存,MySQL也会将结果存放到缓存中。结果集返回客户端是一个增量且逐步返回的过程。有可能MySQL在生成第一条结果时,就开始向客户端逐步返回结果集了。这样服务端就无须存储太多结果而消耗过多内存,也可以让客户端第一时间获得返回结果。

至此,这个语句就执行完成了。对于有索引的表,执行的逻辑也差不多。第一次调用的是“取满足条件的第一行”这个接口,之后循环取“满足条件的下一行”这个接口,这些接口都是引擎中已经定义好的。你会在数据库的慢查询日志中看到一个rows_examined的字段,表示这个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行的时候累加的。

在有些场景下,执行器调用一次,在引擎内部则扫描了多行,因此引擎扫描行数跟rows_examined并不是完全相同的。

回头总结一下MySQL整个查询执行过程,总的来说分为6个步骤:

1.客户端向MySQL服务器发送一条查询请求

2.服务器首先检查查询缓存,如果命中缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段

3.服务器进行SQL解析、预处理

4.由优化器生成对应的执行计划

5.MySQL根据执行计划,调用存储引擎的API来执行查询

6.将结果返回给客户端,同时缓存查询结果

二、存储引擎

存储引擎是指使用不同的存储机制、索引技巧将数据存储在文件或内存中。可以理解为数据的不同存放位置以及不同的文件存储格式

MySQL支持多种存储引擎,如 InnoDB、MyISAM、Memory、Merge、Archive、Federated、CSV、BLACKHOLE 等,常用的是 InnoDB、MyISAM和Memory。InnoDB和MyISAM是将数据以文件的形式存储在磁盘中,Memory是将数据存储在内存中。对于每张表,InnoDB有两个文件存储表数据,以frm为后缀的文件存储的是表结构,以ibd为后缀的文件存储的是表中的数据和索引(如果有索引的话),即InnoDB存储引擎是将表数据和索引存储在同一个文件中的。MyISAM有三个文件存储表数据,以frm为后缀的文件存储的是表结构,以MYD为后缀的文件存储的是表中的数据,以MYI为后缀的文件存储的是索引,即MyISAM存储引擎是将表数据与索引分开存储在两个文件中。

存储引擎

MyISAM

InnoDB

是否支持事务

是否支持外键

锁的级别

表锁(锁定整张表)

行锁(锁定某些行)和表锁

索引

支持全文索引,索引文件与数据文件分离,索引文件以key/value的形式保存在B+Tree中,key是索引字段,value就是数据文件的地址,非聚集索引

不支持全文索引,索引文件就是数据文件,两者不分离,索引与数据以key/value的形式存在B+Tree中,聚集索引

效率

大量的插入操作建议使用MyISAM

大量的修改操作建议使用InnoDB

count函数

保存表的总行数,使用count函数时直接将该值返回,速度很快。如果存在where条件,两种引擎是一样的。

使用count函数时扫描整张表计算表的总行数

存储限制

256TB

64TB

空间使用

内存使用

数据插入速度

InnoDB支持行锁和表锁,是针对索引来说的,如果没有索引,或者查询语句没有用到索引字段,则不使用行锁,而是使用表锁,锁住整张表。

InnoDB行锁是通过给索引加锁实现的,只有通过索引条件检索数据,InnoDB才使用行锁,否则,InnoDB将使用表锁。Oracle中的行锁是通过在数据块中对相应的数据行加锁实现的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值