高性能MySQL学习笔记
高性能MySQL学习笔记
学习来源:高性能MySQL第三版
学习时间:2023年2月18日,2023年6月20日
示例数据库:sakila database
1 架构和历史
1.1 逻辑架构
一共分为三层结构:第一二层为服务器层,第三层为存储引擎层
- 最上层-连接层:实现的功能有:连接处理、授权认证、安全等。
- 第二层-服务层:大多数MySQL的核心服务功能都在这一层,包括查询解析、分析、优化、缓存以及所有的内置函数,所有跨存储引擎的功能都在这一层实现:存储过程、触发器、视图等。
- 第三层-存储引擎层:包含了存储引擎。存储引擎负责MySQL中数据的存储和提取。和GNU/Linux下的各种文件系统一样,每个存储引擎都有它的优势和劣势。服务器通过API与存储引擎进行通信。这些接口屏蔽了不同存储引擎之间的差异,使得这些差异对上层的查询过程透明。存储引擎 API包含几十个底层函数,用于执行诸如”开始一个事务”或者”根据主键提取一行记录”等操作。但存储引擎不会去解析SQL,不同存储引擎之间也不会相互通信,而只是简单地响应上层服务器的请求。
1.1.1 连接层
系统(客户端)访问MySQL
服务器前,做的第一件事就是建立TCP
连接。
经过三次握手建立连接成功后,MySQL
服务器对TCP
传输过来的账号密码做身份认证、权限获取。
连接建立后,每个客户端连接都会在服务器进程中拥有一个线程,这个连接的查询只会在这个单独的线程中执行,该线程只能轮流在某个CPU核心或者CPU中运行。服务器会负责缓存线程,因此不需要为每一个新建的连接创建或者销毁线程。
1.1.2 服务层
- SQL Interface:SQL接口
- 接收用户的SQL命令,并且返回用户需要查询的结果。比如SELECT … FROM就是调用SQL Interface
- MySQL支持DML(数据操作语言)、DDL(数据定义语言)、存储过程、视图、触发器、自定义函数等多种SQL语言接口
Parser: 解析器
- 在解析器中对 SQL 语句进行语法分析、语义分析。将SQL语句分解成数据结构,并将这个结构传递到后续步骤,以后SQL语句的传递和处理就是基于这个结构的。如果在分解构成中遇到错误,那么就说明这个SQL语句是不合理的。
- 在SQL命令传递到解析器的时候会被解析器验证和解析,并为其创建
语法树
,并根据数据字典丰富查询语法树,会验证该客户端是否具有执行该查询的权限
。创建好语法树后,MySQL还会对SQL查询进行语法上的优化,进行查询重写。
Optimizer: 查询优化器
- SQL语句在语法解析之后、查询之前会使用查询优化器确定 SQL 语句的执行路径,生成一个
执行计划
。 - 这个执行计划表明应该
使用哪些索引
进行查询(全表检索还是使用索引检索),表之间的连接顺序如何,最后会按照执行计划中的步骤调用存储引擎提供的方法来真正的执行查询,并将查询结果返回给用户。 - 它使用“
选取-投影-连接
”策略进行查询。例如:
1
SELECT id,name FROM student WHERE gender = '女';
这个SELECT查询先根据WHERE语句进行
选取
,而不是将表全部查询出来以后再进行gender过滤。 这个SELECT查询先根据id和name进行属性投影
,而不是将属性全部取出以后再进行过滤,将这两个查询条件连接
起来生成最终查询结果。- SQL语句在语法解析之后、查询之前会使用查询优化器确定 SQL 语句的执行路径,生成一个
Caches & Buffers: 查询缓存组件
- MySQL内部维持着一些Cache和Buffer,比如Query Cache用来缓存一条SELECT语句的执行结果,如果能够在其中找到对应的查询结果,那么就不必再进行查询解析、优化和执行的整个过程了,直接将结果反馈给客户端。
- 这个缓存机制是由一系列小缓存组成的。比如表缓存,记录缓存,key缓存,权限缓存等 。
- 这个查询缓存可以在
不同客户端之间共享
。 - 从MySQL 5.7.20开始,不推荐使用查询缓存,并在
MySQL 8.0中删除
。
1.1.3 引擎层
插件式存储引擎层( Storage Engines),真正的负责了MySQL中数据的存储和提取,对物理服务器级别维护的底层数据执行操作,服务层通过API与存储引擎进行通信。
MySQL 8.0.25默认支持的存储引擎如下:
1.1.4 小结
简化为三层结构:
连接层:客户端和服务器端建立连接,客户端发送 SQL 至服务器端;
SQL 层(服务层):对 SQL 语句进行查询处理;与数据库文件的存储方式无关;
存储引擎层:与数据库文件打交道,负责数据的存储和读取。
1.2 并发控制
1.2.1 读写锁
在处理并发读或者写时,可以通过实现一个由两种类型的锁组成的锁系统来解决问题。这两种类型的锁通常被称为共享锁(shared lock
)和排他锁(exclusive lock
),也叫读锁(read lock
)和写锁(write lock
)。
- 读锁是共享的,或者说是相互不阻塞的。多个客户在同一时刻可以同时读取同一个资源,而互不干扰。
- 写锁则是排他的,也就是说一个写锁会阻塞其他的写锁和读锁。
在实际的数据库系统中,每时每刻都在发生锁定,当某个用户在修改某一部分数据时,MySQL会通过锁定防止其他用户读取同一数据。大多数时候,MySQL锁的内部管理都是透明的。
1.2.2 锁粒度
所谓的锁策略,就是在锁的开销和数据的安全性之间寻求平衡,这种平衡当然也会影响到性能。大多数商业数据库系统没有提供更多的选择,一般都是在表上施加行级锁(row-level lock
),并以各种复杂的方式来实现,以便在锁比较多的情况下尽可能地提供更好的性能。
而MySQL则提供了多种选择。每种MySQL存储引擎都可以实现自己的锁策略和锁粒度。在存储引擎的设计中,锁管理是个非常重要的决定。将锁粒度固定在某个级别,可以为某些特定的应用场景提供更好的性能,但同时却会失去对另外一些应用场景的良好支持。好在MySQL支持多个存储引擎的架构,所以不需要单一的通用解决方案。下面将介绍两种最重要的锁策略。
① 表锁
表锁(table lock
)是MySOL中最基本的锁策略,并且是开销最小的策略。表锁会锁定整张表,锁粒度大。一个用户在对表进行写操作(插入、删除、更新等)前,需要先获得表的写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他读取的用户才能获得读锁,读锁之间是不相互阻塞的。
写锁比读锁有更高的优先级,因此一个写锁请求可能会被插入到读锁队列的前面(写锁可以插入到锁队列中读锁的前面,反之读锁则不能插入到写锁的前面)。
② 行级锁
行级锁(row lock
)可以最大程度地支持并发处理(同时也带来了最大的锁开销),锁粒度小。在InnoDB
和XtraDB
,以及其他一些存储引擎中实现了行级锁。行级锁只在存储引擎层实现,而MySQL服务器层没有实现。服务器层完全不了解存储引擎中的锁实现。
1.3 事务
1.3.1 ACID
事务(transaction
)就是一组原子性的SQL查询,或者说一个独立的工作单元。如果数据库引擎能够成功地对数据库应用该组查询的全部语句,那么就执行该组查询。如果其中有任何一条语句因为崩溃或其他原因无法执行,那么所有的语句都不会执行。也就是说,事务内的语句,要么全部执行成功,要么全部执行失败。注意,事务与事务之间是可以并发执行的,例如可能先执行事务A的第一条语句,然后执行事务B的第二三条语句等等。
事务的性质:ACID
- 原子性(
atomicity
):一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性。 - 一致性(
consistency
):数据库总是从一个一致性的状态转换到另外一个一致性的状态。 - 隔离性(
isolation
):通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。 - 持久性(
durability
):一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。
用户可以根据业务是否需要事务处理,来选择合适的存储引擎。对于一些不需要事务的查询类应用,选择一个非事务型的存储引擎,可以获得更高的性能。即使存储引擎不支持事务,也可以通过LOCK TABLES
语句为应用提供一定程度的保护,这些选择用户都可以自主决定。
代码示例
假设一个银行的数据库有两张表:支票(checking)表和储蓄(savings)表。现在要从用户Jane的支票账户转移200美元到她的储蓄账户,那么需要至少三个步骤:
- 检查支票账户的余额高于200美元
- 从支票账户余额中减去200美元
- 在储蓄账户余额中增加200美元
可以用START TRANSACTION
语句开始一个事务,然后要么使用COMMIT
提交事务将修改的数据持久保留,要么使用ROLLBACK
撤销所有的修改。事务SQL的样本如下:
1 | START TRANSACTION; |
隔离性:当执行完第三条语句,第四条语句还未开始时,此时有另外一个账户汇总程序开始运行,则其看到的支票(checkings
)账户的余额并没有被减去200美元。
1.3.2 隔离级别
对于隔离性,在SQL标准中定义了四种隔离级别,每一种级别都规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的。较低级别的隔离通常可以执行更高的并发,系统的开销也更低。
READ UNCOMMITTED
(未提交读):在READ UNCOMMITTED级别,事务中的修改,即使没有提交,对其他事务也都是可见的。事务可以读取未提交的数据,这也被称为脏读(Dirty Read
)。这个级别会导致很多问题,在实际应用中一般很少使用。例如:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据。READ COMMITTED
(提交读):大多数数据库系统的默认隔离级别都是READ COMMITTED(但 MySQL不是)。一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。这个级别有时候也叫做不可重复读(nonrepeatable read
),因为两次执行同样的查询,可能会得到不一样的结果。例如:事务A多次读取同一数据,事务B在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。REPEATABLE READ
(可重复读):REPEATABLE READ解决了脏读的问题。该级别保证了在同一个事务中多次读取同样记录的结果是一致的。但是理论上,可重复读隔离级别还是无法解决另外一个幻读(Phantom Read)的问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行(Phantom Row)。InnoDB和XtraDB存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)解决了幻读的问题。可重复读是MySQL的默认事务隔离级别。例如:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。SERIALIZABLE
(可串行化):SERIALIZABLE是最高的隔离级别。它通过强制事务串行执行,避免了前面说的幻读的问题。简单来说,SERIALIZABLE会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用的问题。实际应用中也很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别。
总结:
隔离级别 | 脏读 | 不可重复读 | 幻读 | 加锁读 |
---|---|---|---|---|
READ UNCOMMITTED |
√ | √ | √ | × |
READ COMMITTED |
× | √ | √ | × |
REPEATABLE READ |
× | × | √ | × |
SERIALIZABLE |
× | × | × | √ |
1.3.3 死锁
死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁。多个事务同时锁定同一个资源时,也会产生死锁。例如,设想下面两个事务同时处理StockPrice
表:
1 | # 事务1 |
如果凑巧,两个事务都执行了第一条 UPDATE语句,更新了一行数据,同时也锁定了该行数据(行级锁),接着每个事务都尝试去执行第二条UPDATE语句,却发现该行已经被对方锁定,然后两个事务都等待对方释放锁,同时又持有对方需要的锁,则陷入死循环。除非有外部因素介入才可能解除死锁。
InnoDB 目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚(这是相对比较简单的死锁回滚算法)。
1.3.4 MySQL中的事务
MySOL提供了两种事务型的存储引擎:InnoDB
和NDB Cluster
。另外还有一些第三方存储引擎也支持事务,比较知名的包括XtraDB
和PBXT
。
① 自动提交
MySQL默认采用自动提交(AUTOCOMMIT)模式。也就是说,如果不是显式地开始一个事务,则每个查询都被当作一个事务执行提交操作。在当前连接中,可以通过设置AUTOCOMMIT
变量来启用或者禁用自动提交模式:
1 | SHOW VARIABLES LIKE 'AUTOCOMMIT'; |
1 或者 ON 表示启用,0或者 OFF表示禁用。当AUTOCOMMIT=0
时,所有的查询都是在一个事务中,直到显式地执行COMMIT提交或者ROLLBACK回滚,该事务结束,同时又开始了另一个事务。
设置隔离级别:
1 | SET SESSION TRANSACTION ISOLATION LEVEL XXX; |
② 在事务中混合使用存储引擎
MySQL服务器层不管理事务,事务是由下层的存储引擎实现的。所以在同一个事务中,使用多种存储引擎是不可靠的。
如果在事务中混合使用了事务型和非事务型的表(例如InnoDB和MyISAM表),在正常提交的情况下不会有什么问题。
但如果该事务需要回滚,非事务型的表上的变更就无法撤销,这会导致数据库处于不一致的状态,这种情况很难修复,事务的最终结果将无法确定。所以,为每张表选择合适的存储引擎非常重要。
在非事务型的表上执行事务相关操作的时候,MySQL通常不会发出提醒,也不会报错。有时候只有回滚的时候才会发出一个警告:”某些非事务型的表上的变更不能被回滚”。但大多数情况下,对非事务型表的操作都不会有提示。
③ 隐式和显式锁定
InnoDB采用的是两阶段锁定协议(two-phase locking protocol)。在事务执行过程中,随时都可以执行锁定,锁只有在执行COMMIT或者ROLLBACK的时候才会释放,并且所有的锁是在同一时刻被释放。前面描述的锁定都是隐式锁定,InnoDB会根据隔离级别在需要的时候自动加锁。
补充:两段锁协议
两段锁协议: 是指所有的事务必须分两个阶段对数据项加锁和解锁。
第一阶段是获得封锁的阶段,称为扩展阶段:其实也就是该阶段可以进入加锁操作,在对任何数据进行读操作之前要申请获得读锁,在进行写操作之前要申请并获得写锁,加锁不成功,则事务进入阻塞状态,直到加锁成功才继续执行。
第二阶段是释放封锁的阶段,称为收缩阶段:当事务释放一个封锁后,事务进入封锁阶段,在该阶段只能进行解锁而不能再进行加锁操作。
若并发执行的所有事务均遵守两段锁协议
,则对这些事务的任何并发调度策略都是可串行化
的。
InnoDB也可以使用显式锁定,但是不符合SQL规范:
1 | SELECT ... LOCK IN SHARE MODE; # 显式的共享锁 |
MySQL也支持LOCK TABLES
和UNLOCK TABLES
语句,这是在服务器层实现的,和存储引擎无关。它们有自己的用途,但并不能替代事务处理。如果应用需要用到事务,还是应该选择事务型存储引擎。
1.4 多版本并发控制
暂略
1.5 存储引擎
MySQL体系架构:
1.5.1 简介
在文件系统中,MySQL将每个数据库(也可以称之为schema
)保存为数据目录下的一个子目录。
创建表时,MySQL会在数据库子目录下创建一个和表同名的.frm
文件保存表的定义。例如创建一个名为MyTable
的表,MySQL会在MyTable.frm
文件中保存该表的定义。不同的存储引擎保存数据和索引的方式是不同的,但表的定义则是在MySQL 服务层统一处理的。一张表对应一种存储引擎。
可以使用SHOW TABLE STATUS
命令显示表的相关信息。例如,对于mysql数据库中的user表:
1 | SHOW TABLE STATUS LIKE 'user'; |
查看数据库的存储目录:
1 | mysql> show variables like 'datadir'; |
创建一个数据库test
,然后进入该目录:
1 | CREATE DATABASE test; |
常用的存储引擎有CSV、Archive、Memory、InnoDB、MyISAM。
1.5.2 CSV
CSV存储引擎,数据是以CSV文件(逗号分隔值文件)
存储。
特点:
- 不能定义索引,列定义必须为NOT NULL,不能设置自增列;(缺点:不适用于大表或者数据查询、排序等处理操作)
- CSV数据的存储需要用
,逗号
隔开,可直接编辑 CSV文件进行数据的编排(缺点:数据安全性低) - 可以对硬盘中保存的表文件数据进行直接编辑
xxx.CSV
文件,但是不能将该文件删除然后上传自己的同名.CSV文件
(该文件中可能还有额外一些和表相关的数据),我们只能对其中数据进行编辑操作。编辑之后,通过命令:flush table XXX表名
的方式使其生效
应用场景:
- 数据的快速导入导出;
- 表格直接转换成CSV;
示例
在test
库中创建表user_csv
:
1 | CREATE TABLE `users_csv` ( |
进入datadir/test/
,可看到表的定义:
1.5.3 Archive
Archive存储引擎,是以压缩协议
进行数据的存储。
特点:
- 只支持 insert 和 select 两种操作;
(缺点:不适用于对数据的处理操作)
- 只允许自增ID列建立索引;
(缺点:还是不方便数据的处理)
- 支持行级锁;
- 不支持事务;
- 优点:数据占用磁盘少;
应用场景:
- 日志系统;
- 大量的设备数据采集;
示例
1 | CREATE TABLE `users_archive` ( |
1.5.4 InnoDB
在 MySQL 5.5 及以后版本后,MySQL 选择使用 InnoDB 为默认存储引擎。在创建数据库表时,不指定存储引擎时,使用的就是 InnoDB。如需使用其他存储引擎,可以手动来指定。
特点:
- InnoDB 支持事务操作;(每一条SQL都默认封装成事务,自动提交,会影响速度)
- InnoDB 支持外键;
- InnoDB 是聚集索引(聚簇索引);不仅缓存索引还要缓存真实数据, 对内存要求较高 ,而且内存大小对性能有决定性的影响。
- InnoDB 不保存表的总条数;
- InnoDB 5.7版本之前不支持全文检索;
- InnoDB 支持表级锁、行级锁,默认为行级锁;
- InnoDB 表必须有主键(如果我们没有明确去指定创建主键索引。它会帮我们隐藏的生成一个 6 byte 的 int 型的索引作为主键索引);
- InnoDB 文件存储方式为
.frm
文件存储表结构,.ibd
文件存储数据内容 - 除非有非常特别的原因需要使用其他的存储引擎,否则应该优先考虑InnoDB引擎。
示例
1 | CREATE TABLE `employee` ( |
- .frm文件:保存的是表结构定义描述文件
- .ibd文件:保存的是employee表中的数据内容
1.5.5 MyISAM
MyISAM 作为 MySQL 中 B+Tree 索引的另一种重要体现形式。但MyISAM不支持事务、行级锁、外键
,有一个毫无疑问的缺陷就是崩溃后无法安全恢复
。
特点:
- MyISAM 是非聚集索引;
- MyISAM 有一个变量专门来保存整个表的行数,查询count很快(注意不能加任何 where 条件)
- MyISAM 支持全文索引;
- MyISAM 可以被压缩后进行查询操作,节省空间容量;
- MyISAM 支持表级锁,不支持行级锁;
- MyISAM 中主键不是必须的;
- MyISAM 文件存储方式为
.frm
文件存储表结构,.MYD
文件存储数据内容,.MYI
文件存储索引文件。
示例
1 | CREATE TABLE `employee_myisam` ( |
- .frm文件:保存的是表结构定义描述文件
- .MYD文件:保存的是数据内容
- .MYI文件:保存的是索引内容
MyISAM
和InnoDB
比较
对比项 | MyISAM | InnoDB |
---|---|---|
外键 | 不支持 | 支持 |
事务 | 不支持 | 支持 |
行表锁 | 表锁,即使操作一条记录也会锁住整个表,不适合高并发的操作 | 行锁,操作时只锁某一行,不对其它行有影响,适合高并发的操作 |
缓存 | 只缓存索引,不缓存真实数据 | 不仅缓存索引还要缓存真实数据,对内存要求较高,而且内存大小对性能有决定性的影响 |
自带系统表使用 | Y | N |
关注点 | 性能:节省资源、消耗少、简单业务 | 事务:并发写、事务、更大资源 |
默认安装 | Y | Y |
默认使用 | N | Y |
2 基准测试
暂略
3 服务器性能剖析
暂略
4 Schema与数据类型优化
暂略
5 创建高性能的索引
索引(或者叫做键)是存储引擎用于快速找到记录的一种数据结构。索引优化应该是对查询性能优化最有效的手段了。
5.1 索引基础
假设运行下面的查询:
1 | SELECT first_name FROM sakila.actor WHERE actor_id = 5; |
如果在actor_id
列上建有索引,则MySQL将使用该索引找到actor_id
为5的行,也就是说,MySQL先在索引上按值进行查找,然后返回所有包含该值的数据行。
索引可以包含一个或多个列的值。如果索引包含多个列,那么列的顺序也十分重要,因为MySQL只能高效地使用索引的最左前缀列。创建一个包含两个列的索引,和创建两个只包含一列的索引是大不相同的。
5.1.1 索引的类型
索引有很多种类型,可以为不同的场景提供更好的性能。在MySQL中,索引是在存储引擎层而不是服务器层实现的。所以,并没有统一的索引标准。
① 普通索引
普通索引是mysql里最基本的索引,没有什么特殊性,在任何一列上都能进行创建。
- 创建:
1 | # 语法格式,其中length可以忽略 |
- 删除
1 | DROP INDEX 索引名; |
② 主键索引
mysql会在主键上自动建立一个索引,这就是主键索引。主键是具有唯一性并且不允许为NULL,所以它是一种特殊的唯一索引。一般在建立表的时候选定。
③ 复合索引
又称为组合索引,指的是在建立索引的时候使用多个字段,例如同时使用身份证和手机号建立索引,同样的可以建立为普通索引或者是唯一索引。
1 | CREATE INDEX 索引名 ON 表名(字段1, 字段2, ...); |
复合索引的使用复合最左原则:在使用组合索引的列为条件的时候,是必须要出现最左侧列作为条件,否则索引是不生效的。
例如,在执行以下查询语句时:
1 | SELECT * FROM user_innodb where name = '程冯冯'; # 索引不生效 |
5.1.2 索引的数据结构
略,详见MySQL高级学习笔记
的3.4节
。
5.3 高性能的索引策略
5.3.1 独立的列
独立的列要求索引列不能是表达式的一部分,例如:
1 | SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5; |
这样的查询不能使用actor_id
列的索引。
应当简化where条件:
1 | SELECT actor_id FROM sakila.actor WHERE actor_id = 5; |