该文为《 MySQL 实战 45 讲》的学习笔记,感谢查看,如有错误,欢迎指正
一、MySQL 的基础架构
以下就是 MySQL 的基础架构图。
在 Linux 中安装 MySQL 时,最少需要安装 mysql-server 以及 mysql-client,而服务端中又包含了 Server 层和存储引擎。
Server 层包含了连接器
,查询缓存
,分析器
,优化器
,执行器
,以及内置函数(日期,时间,数学和加密函数等)
,所有跨存储引擎的功能都在这一层实现。比如存储过程
,触发器
,视图
等。
存储引擎层是独立的,可以理解为插件形式,Server 层接入了好几种存储引擎,比如MyISAM
、InnoDB
、Memory
等,MySQL 5.5.5 之后默认的存储引擎是InnoDB
。不管你的 MySQL 使用了多少种存储引擎,它们都是共享一个 Server 层。
如果在建表时(create table
),想指定使用其它引擎,可以加上engine=MyISAM
实现。
二、查询语句的执行过程
语句示例:
mysql> select * from T where id=10
2.1 连接器
连接器负责接收处理客户端发送过来的连接请求,获取权限,维持和管理连接。
客户端可以使用 mysql-client,通过命令行mysql -uroot -p
进行建立连接。也可以使用第三方工具如Navicat
建立连接。连接过程也是走的TCP/IP协议,有经典的3次握手过程。
连接器先判断用户名密码是否正确,不正确会返回Access denied for user
错误;正确的话,会到权限表中查询出该用户的权限,只要该连接未断开,将会一直使用该权限。
这也就是为什么有时候我们即使修改了用户的权限,也不会立刻生效,必须断开重连,才能生效。当然如果该连接长时间处于空闲状态(连接上以后没有动作),默认 8 小时以后就会自动断开该连接。这个 8 小时是由wait_timeout
来控制的。
通过show processlist
可以查看哪些连接处于空闲状态,Command
为Sleep
的就是空闲连接,可以通过kill Id
来手动断开连接,一般在死锁或者事务阻塞的时候会用到。
mysql> show processlist;
+----+---------+-----------------+------+---------+------+-------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+---------+-----------------+------+---------+------+-------+------------------+
| 3 | root | localhost:64511 | NULL | Query | 0 | init | show processlist |
| 4 | ruhrbim | localhost:64519 | NULL | Sleep | 3 | | NULL |
+----+---------+-----------------+------+---------+------+-------+------------------+
2 rows in set (0.00 sec)
mysql>
MySQL 5.7
及更新版本,可以使用mysql_reset_connection
来初始化连接资源,这个操作不需要重新做权限验证,直接恢复到刚创建完连接时的状态。
2.2 查询缓存
查询缓存中存储的是之前的查询语句及结果,以key-value
的形式存储,key
是查询语句,value
是查询结果。在执行select
语句之前,会先去查询缓存看一下,如果有结果,直接从查询缓存返回,不经过后面的分析器、优化器、执行器等。如果没有结果,就会往后依次执行一遍,最后把语句及结果存入查询缓存,以便下次使用。
查询缓存主要针对的是变动不频繁的表,只要表发生了变更,那么这个表上的查询缓存都会被清空。MySQL也提供了"按需使用"的方法,将query_cache_type
设置为DEMAND
,这样默认 SQL 语句都不使用查询缓存,要使用的时候,可以通过SQL CACHE
显式指定。
mysql> select SQL_CACHE * from T where ID=10;
Tips:MySQL 8.0 将查询缓存功能直接去掉了
2.3 分析器
没有命中查询缓存,就会到分析器这一步。分析器是对 SQL 语句做解析的。
首先进行「词法分析」,根据select
判断是一个查询语句,还要把字符串T
识别为表名 T
,字符串ID
识别为列 ID
。
然后进行「语法分析」,分析这一行 SQL 语句是否满足 MySQL 语法。
mysql> select * fron huanzi;
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 'fron huanzi' at line 1
mysql>
我们把from
写成了fron
,分析器做语法分析时识别出这里有问题,并将问题点定位出来了,在use near
后面就是。
2.4 优化器
现在 MySQL 已经知道要做什么了,但在开始执行 SQL 语句之前还要经过优化器的处理。
优化器能够选择使用哪个索引,或者在多表关联的时候,选择连接的顺序。
当然有时候优化器也会选择错索引,我们可以使用force index(有索引的列名)
来强制指定使用某一个索引。
mysql> select * from t force index(a) where a between 10000 and 20000;
2.5 执行器
SQL 语句经过了以上步骤,最终到达执行器,执行器的作用就是执行 SQL 语句。
开始执行的时候,要先判断一下是否有执行该语句的权限,没有权限会返回错误。
mysql> select * from T where ID=10;
ERROR 1142 (42000): SELECT command denied to user 'b'@'localhost' for table 'T
mysql>
如果命中了查询缓存,不走执行器,也会在查询缓存返回结果的时候做权限验证。
如果有权限,就继续执行,以上述查询语句为例,执行流程如下:
- 调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是 10,如果不是则跳过,如果是则将这行存在结果集中;
- 调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
- 执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。
三、更新语句的执行过程
给出一个表的建表语句:
mysql> create table T(ID int primary key, c int);
更新语句也有查询语句的那些流程,以update T set c=c+1 where ID=2;
为例,首先连接到连接器,然后将表T
上的缓存全部清空,分析器分析以后知道这是一个更新语句,优化器决定使用 ID 这个索引。执行器负责执行语句。
除了以上步骤,更新语句还多了 2 个日志模块,分别是redo log
(重做日志)和binlog
(归档日志)。
3.1 redo log
更新的时候,如果每次都要去磁盘找到那条记录,并且直接更新至磁盘,会产生很大的 IO 成本,在 MySQL 中有 1 个 WAL 技术就是为了解决这个问题,全称叫做 Write-Ahead Logging
,关键点在于先写日志,再写磁盘。
具体来说,在更新数据库的时候,InnoDB 引擎会先把记录写到 redo log 里面,并更新内存,这时候更新就算已经完成了,InnoDB 引擎会在适当的时候,将记录更新到磁盘,一般是在服务器负载较低的时候。
InnoDB 里面的 redo log 是固定大小的,可以在/etc/my.cnf
中进行配置,一般是 1 组 4 个文件,文件名是ib-logfile-0
、ib-logfile-1
、ib-logfile-2
、ib-logfile-3
,会从 0 到 3 开始循环写,在 3 写满之后又会向 0 里面写,因此要永远保证 redo log 中有剩余空间可以记录信息,如果已经写满了,就会停下来先刷一部分数据到磁盘,空间腾出来以后,继续记录。
write pos 是当前记录的位置,checkpoint 是当前要擦除的位置。write pos 和 checkpoint 之间的是“粉板”上还空着的部分,可以用来记录新的操作。
有了 redo log,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。
3.2 binlog
redo log 是 InnoDB 独有的功能,在 MySQL 还没有引入 InnoDB 的时候,也有一个日志,就是 binlog 日志,主要功能是归档,以及主从复制使用。crash-safe 和 WAL 也都是 InnoDB 特有的。
- binlog 是 Server 层的日志,并不是引擎层,因此所以的引擎都可以使用 binlog 日志。
- redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑
- redo log 是循环写的,空间固定会用完;binlog 是可以追加写入的,不会覆盖旧的文件。
回到更新语句中,InnoDB 将数据写入 redo log 后还没结束,此时 redo log 处于 prepare 状态;
然后 InnoDB 告知执行器执行完成了,随时可以提交事务,执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。