从根儿上理解MySQL


1.MySQL的客户端/服务器架构

我们的MySQL服务器程序和客户端程序本质上都算是计算机上的一个进程,这个代表着MySQL服务器程序的进程也被称为MySQL数据库实例,简称数据库实例

启动的MySQL服务器进程的默认名称为mysqld, 而我们常用的MySQL客户端进程的默认名称为mysql

2.启动MySQL客户端程序

image-20201026110543308

bin目录下有许多客户端程序,比方说mysqladminmysqldumpmysqlcheck等等,这里我们重点关注的是可执行文件mysql,通过这个可执行文件可以让我们和服务器程序进程交互,也就是发送请求,接收服务器的处理结果。启动这个可执行文件时一般需要一些参数,格式如下:

mysql -h用户名 -u用户名 -p密码

各个参数的意义如下:

image-20201026110704495

2.1 启动客户端

mysql -hlocalhost -uroot -p283619
image-20201026110850305

最后一行的mysql>是一个客户端的提示符,之后客户端发送给服务器的命令都需要写在这个提示符后边。

2.2 关闭客户端

断开客户端与服务器的连接并且关闭客户端,可以在mysql>提示符后输入下边任意一个命令:

  1. quit
  2. exit
  3. \q
image-20201026111148698

3.客户端与服务器连接的过程

客户端进程向服务器进程发送请求并得到回复的过程本质上是一个进程间通信的过程!MySQL支持下边三种客户端进程和服务器进程的通信方式。

3.1 TCP/IP

真实环境中,数据库服务器进程和客户端进程可能运行在不同的主机中,它们之间必须通过网络来进行通讯。MySQL采用TCP作为服务器和客户端之间的网络通信协议。在网络环境下,每台计算机都有一个唯一的IP地址,如果某个进程有需要采用TCP协议进行网络通信方面的需求,可以向操作系统申请一个端口号,这是一个整数值,它的取值范围是0~65535。这样在网络中的其他进程就可以通过IP地址 + 端口号的方式来与这个进程连接,这样进程之间就可以通过网络进行通信了。

MySQL服务器启动的时候会默认申请3306端口号,之后就在这个端口号上等待客户端进程进行连接,用书面一点的话来说,MySQL服务器会默认监听3306端口。

如果3306端口号已经被别的进程占用了或者我们单纯的想自定义该数据库实例监听的端口号,那我们可以在启动服务器程序的命令行里添加-P参数(大写的)来明确指定一下端口号,比如这样:

mysqld -P3307

这样MySQL服务器在启动时就会去监听我们指定的端口号3307

如果客户端进程想要使用TCP/IP网络来连接到服务器进程,比如我们在使用mysql来启动客户端程序时,在-h参数后必须跟随IP地址来作为需要连接的服务器进程所在主机的主机名,如果客户端进程和服务器进程在一台计算机中的话,我们可以使用127.0.0.1来代表本机的IP地址。另外,如果服务器进程监听的端口号不是默认的3306,我们也可以在使用mysql启动客户端程序时使用-P参数(大写的P,小写的p是用来指定密码的)来指定需要连接到的端口号。比如我们现在已经在本机启动了服务器进程,监听的端口号为3307,那我们启动客户端程序时可以这样写:

mysql -h127.0.0.1 -uroot -P3307 -p密码

3.2 命名管道和共享内存

如果你是一个Windows用户,那么客户端进程和服务器进程之间可以考虑使用命名管道共享内存进行通信。不过启用这些通信方式的时候需要在启动服务器程序和客户端程序时添加一些参数:

  • 使用命名管道来进行进程间通信

    需要在启动服务器程序的命令中加上--enable-named-pipe参数,然后在启动客户端程序的命令中加入--pipe或者--protocol=pipe参数。

  • 使用共享内存来进行进程间通信

    需要在启动服务器程序的命令中加上--shared-memory参数,在成功启动服务器后,共享内存便成为本地客户端程序的默认连接方式,不过我们也可以在启动客户端程序的命令中加入--protocol=memory参数来显式的指定使用共享内存进行通信。

    不过需要注意的是,使用共享内存的方式进行通信的服务器进程和客户端进程必须在同一台Windows主机中。

注意:命名管道共享内存Windows操作系统中的两种进程间通信方式

3.3 Unix域套接字文件

如果我们的服务器进程和客户端进程都运行在同一台操作系统为类Unix的机器上的话,我们可以使用Unix域套接字文件来进行进程间通信。如果我们在启动客户端程序的时候指定的主机名为localhost,或者指定了--protocol=socket的启动参数,那服务器程序和客户端程序之间就可以通过Unix域套接字文件来进行通信了。MySQL服务器程序默认监听的Unix域套接字文件路径为/tmp/mysql.sock,客户端程序也默认连接到这个Unix域套接字文件。如果我们想改变这个默认路径,可以在启动服务器程序时指定socket参数,就像这样:

mysqld --socket=/tmp/a.txt

这样服务器启动后便会监听/tmp/a.txt。在服务器改变了默认的UNIX域套接字文件后,如果客户端程序想通过UNIX域套接字文件进行通信的话,也需要显式的指定连接到的UNIX域套接字文件路径,就像这样:

mysql -hlocalhost -uroot --socket=/tmp/a.txt -p

这样该客户端进程和服务器进程就可以通过路径为/tmp/a.txtUnix域套接字文件进行通信了。

4.服务器处理客户端请求

其实不论客户端进程和服务器进程是采用哪种方式进行通信,最后实现的效果都是:客户端进程向服务器进程发送一段文本(MySQL语句),服务器进程处理后再向客户端进程发送一段文本(处理结果)。那服务器进程对客户端进程发送的请求做了什么处理,才能产生最后的处理结果呢?客户端可以向服务器发送增删改查各类请求,我们这里以比较复杂的查询请求为例来画个图展示一下大致的过程:

image-20201026113137436

从图中我们可以看出,服务器程序处理来自客户端的查询请求大致需要经过三个部分,分别是连接管理解析与优化存储引擎。下边我们来详细看一下这三个部分都干了什么。

4.1 连接管理

客户端进程可以采用我们上边介绍的TCP/IP命名管道或共享内存Unix域套接字这几种方式之一来与服务器进程建立连接,每当有一个客户端进程连接到服务器进程时,服务器进程都会创建一个线程来专门处理与这个客户端的交互,当该客户端退出时会与服务器断开连接,服务器并不会立即把与该客户端交互的线程销毁掉,而是把它缓存起来,在另一个新的客户端再进行连接时,把这个缓存的线程分配给该新客户端。这样就起到了不频繁创建和销毁线程的效果,从而节省开销。从这一点大家也能看出,MySQL服务器会为每一个连接进来的客户端分配一个线程,但是线程分配的太多了会严重影响系统性能,所以我们也需要限制一下可以同时连接到服务器的客户端数量

在客户端程序发起连接的时候,需要携带主机信息、用户名、密码,服务器程序会对客户端程序提供的这些信息进行认证,如果认证失败,服务器程序会拒绝连接。另外,如果客户端程序和服务器程序不运行在一台计算机上,我们还可以采用使用了SSL(安全套接字)的网络连接进行通信,来保证数据传输的安全性。

当连接建立后,与该客户端关联的服务器线程会一直等待客户端发送过来的请求,MySQL服务器接收到的请求只是一个文本消息,该文本消息还要经过各种处理

4.2 解析与优化

到现在为止,MySQL服务器已经获得了文本形式的请求,接着 还要经过九九八十一难的处理,其中的几个比较重要的部分分别是查询缓存语法解析查询优化,下边我们详细来看。

4.2.1 查询缓存

虽然查询缓存有时可以提升系统性能,但也不得不因维护这块缓存而造成一些开销,比如每次都要去查询缓存中检索,查询请求处理完需要更新查询缓存,维护该查询缓存对应的内存区域。从MySQL 5.7.20开始,不推荐使用查询缓存,并在MySQL 8.0中删除。

如果我问你9+8×16-3×2×17的值是多少,你可能会用计算器去算一下,或者牛逼一点用心算,最终得到了结果35,如果我再问你一遍9+8×16-3×2×17的值是多少,你还用再傻呵呵的算一遍么?我们刚刚已经算过了,直接说答案就好了。MySQL服务器程序处理查询请求的过程也是这样,会把刚刚处理过的查询请求和结果缓存起来,如果下一次有一模一样的请求过来,直接从缓存中查找结果就好了,就不用再傻呵呵的去底层的表中查找了。这个查询缓存可以在不同客户端之间共享,也就是说如果客户端A刚刚查询了一个语句,而客户端B之后发送了同样的查询请求,那么客户端B的这次查询就可以直接使用查询缓存中的数据。

当然,MySQL服务器并没有人聪明,如果两个查询请求在任何字符上的不同(例如:空格、注释、大小写),都会导致缓存不会命中。另外,如果查询请求中包含某些系统函数、用户自定义变量和函数、一些系统表,如 mysql 、information_schema、 performance_schema 数据库中的表,那这个请求就不会被缓存。以某些系统函数举例,可能同样的函数的两次调用会产生不一样的结果,比如函数NOW,每次调用都会产生最新的当前时间,如果在一个查询请求中调用了这个函数,那即使查询请求的文本信息都一样,那不同时间的两次查询也应该得到不同的结果,如果在第一次查询时就缓存了,那第二次查询的时候直接使用第一次查询的结果就是错误的!

不过既然是缓存,那就有它缓存失效的时候。MySQL的缓存系统会监测涉及到的每张表,只要该表的结构或者数据被修改,如对该表使用了INSERTUPDATEDELETETRUNCATE TABLEALTER TABLEDROP TABLEDROP DATABASE语句,那使用该表的所有高速缓存查询都将变为无效并从高速缓存中删除!

4.2.2 语法解析

如果查询缓存没有命中,接下来就需要进入正式的查询阶段了。因为客户端程序发送过来的请求只是一段文本而已,所以MySQL服务器程序首先要对这段文本做分析,判断请求的语法是否正确,然后从文本中将要查询的表、各种查询条件都提取出来放到MySQL服务器内部使用的一些数据结构上来。

4.2.3 查询优化

语法解析之后,服务器程序获得到了需要的信息,比如要查询的列是哪些,表是哪个,搜索条件是什么等等,但光有这些是不够的,因为我们写的MySQL语句执行起来效率可能并不是很高,MySQL的优化程序会对我们的语句做一些优化,如外连接转换为内连接、表达式简化、子查询转为连接吧啦吧啦的一堆东西。优化的结果就是生成一个执行计划,这个执行计划表明了应该使用哪些索引进行查询,表之间的连接顺序是啥样的。我们可以使用EXPLAIN语句来查看某个语句的执行计划,关于查询优化这部分的详细内容我们后边会仔细唠叨,现在你只需要知道在MySQL服务器程序处理请求的过程中有这么一个步骤就好了。

4.3 存储引擎

截止到服务器程序完成了查询优化为止,还没有真正的去访问真实的数据表,MySQL服务器把数据的存储和提取操作都封装到了一个叫存储引擎的模块里。我们知道是由一行一行的记录组成的,但这只是一个逻辑上的概念,物理上如何表示记录,怎么从表中读取数据,怎么把数据写入具体的物理存储器上,这都是存储引擎负责的事情。为了实现不同的功能,MySQL提供了各式各样的存储引擎,不同存储引擎管理的表具体的存储结构可能不同,采用的存取算法也可能不同。

存储引擎的功能就是接收上层传下来的指令,然后对表中的数据进行提取或写入操作。

为了管理方便,人们把连接管理查询缓存语法解析查询优化这些并不涉及真实数据存储的功能划分为MySQL server的功能,把真实存取数据的功能划分为存储引擎的功能。各种不同的存储引擎向上边的MySQL server层提供统一的调用接口(也就是存储引擎API),包含了几十个底层函数,像"读取索引第一条内容"、“读取索引下一条内容”、"插入记录"等等。

所以在MySQL server完成了查询优化后,只需按照生成的执行计划调用底层存储引擎提供的API,获取到数据后返回给客户端就好了。

5.常用存储引擎

MySQL支持非常多种存储引擎,我这先列举一些:

存储引擎描述
ARCHIVE用于数据存档(行被插入后不能再修改)
BLACKHOLE丢弃写操作,读操作会返回空内容
CSV在存储数据时,以逗号分隔各个数据项
FEDERATED用来访问远程表
InnoDB具备外键支持功能的事务存储引擎
MEMORY置于内存的表
MERGE用来管理多个MyISAM表构成的表集合
MyISAM主要的非事务处理存储引擎
NDBMySQL集群专用存储引擎

我们最常用的就是InnoDBMyISAM,有时会提一下Memory。其中InnoDBMySQL默认的存储引擎,我们之后会详细唠叨这个存储引擎的各种功能,现在先看一下一些存储引擎对于某些功能的支持情况:

6.关于存储引擎的一些操作

6.1 查看当前服务器程序支持的存储引擎

我们可以用下边这个命令来查看当前服务器程序支持的存储引擎:

SHOW ENGINES;

来看一下调用效果:

image-20201026122457701

其中的Support列表示该存储引擎是否可用,DEFAULT值代表是当前服务器程序的默认存储引擎。Comment列是对存储引擎的一个描述。Transactions列代表该存储引擎是否支持事务处理。XA列代表着该存储引擎是否支持分布式事务。Savepoints代表着该存储引擎是否支持部分事务回滚。

6.2 设置表的存储引擎

存储引擎是负责对表中的数据进行提取和写入工作的,我们可以为不同的表设置不同的存储引擎,也就是说不同的表可以有不同的物理存储结构,不同的提取和写入方式。

6.3 创建表时指定存储引擎

我们之前创建表的语句都没有指定表的存储引擎,那就会使用默认的存储引擎InnoDB。如果我们想显式的指定一下表的存储引擎,那可以这么写:

CREATE TABLE 表名(
    建表语句;
) ENGINE = 存储引擎名称;

比如我们想创建一个存储引擎为MyISAM的表可以这么写:

mysql> CREATE TABLE engine_demo_table(
    ->     i int
    -> ) ENGINE = MyISAM;
Query OK, 0 rows affected (0.02 sec)

6.4 修改表的存储引擎

如果表已经建好了,我们也可以使用下边这个语句来修改表的存储引擎:

ALTER TABLE 表名 ENGINE = 存储引擎名称;

比如我们修改一下engine_demo_table表的存储引擎:

mysql> ALTER TABLE engine_demo_table ENGINE = InnoDB;
Query OK, 0 rows affected (0.05 sec)
Records: 0  Duplicates: 0  Warnings: 0

这时我们再查看一下engine_demo_table的表结构:

mysql> SHOW CREATE TABLE engine_demo_table\G
*************************** 1. row ***************************
       Table: engine_demo_table
Create Table: CREATE TABLE `engine_demo_table` (
  `i` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.01 sec)

可以看到该表的存储引擎已经改为InnoDB了。


版权声明:本文为sj15814963053原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。