最近做的项目对集群数据库性能要求较高,借此机会打算重新学习、整理Mysql的原理及调优策略,参考梳理《高性能MySQL》第三版、MySQL官方文档、网络上前人写的资料、以及个人实战经验总结。在接下来的一段时间内持续更新,希望在提升自己的同时能帮到同在数据库调优的苦海中挣扎的程序猿们。(取这个标题是因为笔者屡次面试都鼓吹自己懂高性能数据库,不想打脸)
先上一幅MySQL服务器逻辑架构图
如上图所示,MySQL的逻辑架构大致分三层:
第一层 (通信层):并非MySQL所独有,诸如:连接处理、授权认证、安全等功能均在这一层处理。
第二层(服务层):MySQL大多数核心服务均在这一层,包括查询解析、分析、优化、缓存、内置函数(比如:时间、数学、加密等函数)。所有的跨存储引擎的功能也在这一层实现:存储过程、触发器、视图等。
第三层(引擎层):最下层为存储引擎,其负责MySQL中的数据存储和提取。每种存储引擎都有其优势和劣势。中间的服务层通过API与存储引擎通信,这些API接口屏蔽了不同存储引擎间的差异。注意:存储引擎不会去解析SQL,不同存储引擎自检不会相互通信,只会单纯的响应上层的请求。
(更加详尽的流程说明请移步此博客:https://blog.csdn.net/z_ryan/article/details/82260663)
每个连接会在服务器进程中拥有一个连接,这个连接的查询只会在这个单独的线程中执行。服务器会负责缓存连接(MySQL5.5及之后的版本提供了一个API,支持线程池)。一旦客户端连接成功,服务及会继续验证该客户端是否具有执行某个特定查询的权限。
MySQL会解析sql语句,并创建内部数据结构(解析树),染回对其进行优化(包括重写查询、决定表的读取顺序、选择合适的引擎等)。用户可以用关键字hint(提示)和explain(解释)分别提示优化器的决策和解释优化过程中各个因素。
对于select语句,在解析查询之前。服务器会先检查查询缓存(Query Cache),如果能够在其中找到对应的查询,直接返回查询缓存中的结果集(后继更新的博客会详细介绍)。
多个数据在需要再统一时刻修改数据,都会产生并发控制的问题。
可以通过一个有两种类型的锁组成的锁系统来解决问题。这两种类型的锁通常被称为共享锁和排他锁,也叫读锁和写锁。
锁粒度越小就能支持越多的并发。但是加锁需要消耗资源,所谓锁策略就是在锁的开销和数据的并发性能之间寻求平衡。而MySQL提供了多种原则,每种存储引擎都可以实现自己的锁策略和锁粒度。并且MySQL支持多个存储引擎的架构,所以不需要单一的通用解决方案。
两种最重要的锁策略:
表锁(table lock):开销最小,性能最差。
行级锁(row lock):开销最大,最大程度的支持并发处理。
事务就是一组原子性的SQL查询,或者说是一个独立的工作单元。
原子性(Atomicity):一个事物必须被视为一个不可分割的最小单元,要么全部执行成功提交,要么全部执行失败回滚。(银行转账中途失败,不会扣钱)
一致性(Consistency):数据库总是从一个一致的状态转换到另一个一致的状态。(银行转账不管成功失败,双方总金额一致)
隔离性(Isolation):一个事物所做的修改在最终提交之前,对其他事物不可见。(银行转账成功之前,其他程序查询双方的余额并没有改变)
持久性(Durability):一旦事物提交,所做的修改永久保存到数据库中。(不用解释了吧)
注意:像锁粒度的升级会增加系统开销一样,ACID安全性的实现程度越高,数据库系统做的额外工作也越多。
SQL标准中定义了四种隔离级别:
READ UNCOMMITTED(未提交读):事务中的修改,即时没有提交,对其他事物也是可见的。会出现脏读可能性。
READ COMMITTED(提交读):一个事物从开始知道提交之前,所做的修改对其他事物是不可见的。会出现不可重复读可能性。
REPEATABLE READ(可重复读):保证同一事物中多次读取同样记录的结果是一致的(通过保存快照实现)。会出现幻读可能性。
SERIALIZABLE(可串行读):最高隔离级别,会在读取的每行数据上都加锁。
大多数数据库的默认隔离级别都是READ COMMITTED(提交读),但是MySQL的默认隔离级别是REPEATABLE READ(可重复读)。
使用事务日志是为了提高事务的效率。存储引擎在修改表的数据时只需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志中,而不是将修改的数据本身持久到磁盘。事务日志采用的是追加的方式,因此写日志的操作是磁盘上一小块区域中顺序I/O,而不像随机I/O需要再磁盘的多个地方移动磁头,所以采用事务日志的方式相对要快很多。
事务日志持久以后,内存中被修改的数据在后台可以慢慢的刷回到磁盘。目前大多数存储引擎都是这样实现的,我们通常称之为预写式日志,修改日志需要写两次磁盘。如果数据的修改已经激励到事务日志持久化,但数据本身还没有写回磁盘,此时系统崩溃,存储引擎在启动时能够自动恢复这部分修改的数据。
MySQL提供了两种事务型的存储引擎:InnoDB和NDB Cluster。另外还有一些第三方存储引擎也支持事务,如XtraDB和PBXT。
MySQL默认采用自动提交(AUTOCOMMIT)模式。可以通过设置 AUTOCOMMIT 变量来启用或者禁用自动提交模式:
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.01 sec)
mysql> set autocommit = 1;
Query OK, 0 rows affected (0.00 sec)
1或者ON表示启用,0或者OFF表示禁用。当自动提交禁用时,所有查询都会在一个事务中,指导显式的执行commit提交或者rollback回滚,该事务结束,同时又开始了另一个新事物。修改autocommit对非事务型的表(比如MyISAM或者内存表)不会有任何影响。
还有一些命令,会在执行之前强制执行commit提交。经典的例子是ALTER TABLE(DDL)和LOCK TABLES。
MySQL可以通过 SET TRANSACTION ISOLATION LEVEL 命令来设置隔离级别。也可以自改变当前会话的隔离级别:
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
MySQL服务器层不管理事务,事务是由下层的存储引擎实现的。如果在事务中混合使用了事务型和非事务型的表,如果正常提交就没有什么问题,但是如果需要回滚,非事务型的表上的变更将无法撤销。
3.4.3 显式锁定和隐式锁定
InnoDB采用的是两阶段锁定协议(two-phase locking protocol)。
隐式锁定:事务执行过程中,随时都可以执行锁定,锁只有在commit或者rollback的时候才会释放。InnoDB会根据隔离级别在需要的时候自动加锁
显式锁定:特定的语句进行显式锁定:
SELECT ... LOCK IN SHARE MODE --PS:不属于SQL规范
SELECT ... FOR UPDATE --PS:不属于SQL规范
LOCK TABLES ...
UNLOCK TABLES ...
MySQL的大多数事务型存储引擎实现的都不是简单的行级锁。基于提升并发性能的考量,他们一般都同时实现了多版本并发控制(避免了很多加锁操作,因此开销很低,同时也实现了非阻塞的操作,写操作也只锁定必要的行)。
InnoDB的MVCC,是通过在每行记录后面保存两个隐藏的列来实现的。这两个列一个保存了行的创建时间,一个保存了行的过期时间(这里的时间并不是实际的时间值,而是版本号)。没开始一个新事物,系统版本号会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的版本号进行比较。
以REPEATABLE READ 隔离级别下的MVCC的具体操作为例:
SELECT
InnoDB检查每行,要确定它符合两个标准。
InnoDB必须知道行的版本号,这个行的版本号至少要和事物版本号一样的老。(也就是是说它的版本号可能少于或者和事物版 本号相同)。这个既能确定事物开始之前行是存在的,也能确定事物创建或修改了这行。
行的删除操作的版本一定是未定义的或者大于事物的版本号。确定了事物开始之前,行没有被删除。
符合了以上两点。会返回查询结果。
INSERT
InnoDB记录了当前新增行的系统版本号。
DELETE
InnoDB记录的删除行的系统版本号作为行的删除ID。
UPDATE
InnoDB复制了一行。这个新行的版本号使用了系统版本号。它也把系统版本号作为了删除行的版本。
在文件系统中,MySQL将每个数据库(schema)保存为数据目录下的一个子目录,创建表时,MySQL会在数据库子目录下创建一个和表同名的.frm文件保存表的定义。
可以使用 SHOW TABLE STAUS 命令显示表的信息:
mysql> show table status like 'device'\G; --小技巧:加上\G更方便看
*************************** 1. row ***************************
Name: device --表名
Engine: InnoDB --存储引擎
Version: 10 --什么版本?对不起,不认识
Row_format: Dynamic --行的格式:Dynamic(行长度可变)、Fixed
Rows: 36 --目前有多少行
Avg_row_length: 455 --平均每行字节数
Data_length: 16384 --表数据大小(字节)
Max_data_length: 0 --表数据的最大容量(0是无限吧)
Index_length: 16384 --索引大小(字节)
Data_free: 0 --已分配但是未使用的空间
Auto_increment: 37 --下一个自增序列的值(AUTO_INCREMENT)
Create_time: 2018-08-16 17:17:02 --创建时间
Update_time: NULL --最新修改时间
Check_time: NULL --检查时间(CHECK TABLE 命令)
Collation: utf8_bin --默认字符集_字符列排序规则
Checksum: NULL --如果启用,保存的是整个表的实时校验和
Create_options: --其他指定选项
Comment: 设备表
1 row in set (0.00 sec)
1)InnoDB是MySQL的默认事务型引擎,也是最重要的、使用最广泛的引擎。InnoDB的性能和自动崩溃恢复特性,使得它在非事务型存储的需求中也很有用。
2)InnoDB的数据存储在表空间中,表空间是InnoDB管理的一个黑盒子,由一系列的数据文件组成。
3)InnoDB采用MVCC来支持高并发,并实现了四个标准的隔离级别。其默认级别是REPEATEBLE READ(可重复读),并且通过间隙锁(next-key locking)策略防止幻读的出现。间隙锁使得InnoDB不仅仅锁定查询涉及的行,还对索引中的间隙进行锁定,以防止幻影行的插入。
4)InnoDB表是基于聚簇索引建立的。(后面的更新会详细讨论聚簇索引)
5)InnoDB内部做了很多优化,包括从磁盘读取数据时采用的可预测性预读。
在MySQL5.1及之前的版本,MySQL是默认的存储引擎。MyISAM不支持事务和行级锁(MyISAM对整张表加锁,而不针对行),缺陷是崩溃后无法安全恢复。MyISAM引擎设计简单,数据以紧密格式存储,所以在某些场景下的性能很好。MyISAM有一些服务器级别的性能扩展限制,比如对索引键缓冲区(key cache)的Mutex锁,MariaDB基于段的索引键缓冲区机制来避免该问题。但MyISAM最经典的性能问题还是表锁的问题。