Chapter 1: MySQL 架构
1.1 逻辑架构
最上层是客户端,中间是核心服务功能(查询解析、分析、优化、缓存、内置函数,存储过程、触发器、视图再这里实现),最下层是存储引擎。
- 客户端连接会在服务器进程中拥有一个线程,查询只在这个单独的线程中运行。而服务器会缓存线程,不用显式创建/销毁
- MySQL会解析查询并创建解析树,并对其优化(重写查询、更改读取顺序、选合适索引等)。可用hint优化器影响决策,也可以通过
EXPLAIN
查看服务优化的决策 SELECT
语句查询前会检查缓存来加速查找
1.2 并发控制
-
锁种类:S锁(Read), X锁(Write)。只有S与S是不互斥的,其它组合都互斥。
-
锁粒度:表锁(粒度大,开销小,并发度低,如
ALTER TABLE
会上表锁),行锁(粒度小,开销大,并发度高,在某些特殊的存储引擎中实现)
1.3 事务
ACID:原子性,一致性(状态一致),隔离性(并发但感觉不到其它事务的存在),持久性。
用BEGIN
/START TRANSACTION
开启事务, COMMIT
提交事务,ROLLBACK
回滚事务。
-
隔离级别:MySQL定义了4种
READ UNCOMMIT
:事务的修改,即使没提交,修改也对其它事务可见(即脏读)READ COMMIT
: 事务只能看到已提交事务的修改(解决脏读)。但是一个事务到提交前可能会出现2个相同的查询,不同的结果(即不可重复读)REPEATABLE READ
: 保证同个事务中相同查询结果是一致的(解决不可重复读)。但当别的事务在本事务范围内插入新记录,本事务会感知到这个记录的存在,即幻读。InnoDB通过MVCC解决了幻读问题(默认不开启)。(Case: 2个事务A和B,都没提交,A修改了行a,B去读a还是原来的值,因为a加了X锁,B无法加S锁所以去读了快照值;若B去修改a会阻塞,因为加不了X锁。若A提交了,B去改,条件可能会失效,条件是不会适配快照的数据)SERIALIZABLE
:强制事务串行执行,读和写都会对数据(每一行)加锁,解决幻读问题。
- 死锁:InnoDB会检测死锁的循环依赖,抛出错误;以及实现死锁超时机制。死锁错误产生后,InnoDB将持有最好行级X锁的事务回滚。
- 事务日志:采用追加方式,磁盘I/O是顺序的。日志采用Write-Ahead,写入并持久化日志后,数据可以慢慢写回磁盘,即使崩溃了也可以恢复(参考《数据库系统概念》第16章)
- MySQL中的事务
- 自动提交:
AUTOCOMMIT
默认开启,认为每个语句都是一个事务。而对于不支持事务的引擎(如MyISAM)修改它没任何影响,因为没有事务的概念。 - 事务控制是由下层的存储引擎实现的,当使用非事务存储引擎,那么它不会回滚/恢复。
- 2PL:InnoDB使用2PL,只在
COMMIT
或者ROLLBACK
后才释放掉所有的锁,且同时释放。 - 显式锁定:
SELECT ... LOCK IN SHARE MODE/FOR UPDATE
显式加行级IS/IX锁(意向锁)。IS与IX之间是能相容的,但是IS不能与X容,IX不能与S和X相容。LOCK/UNLOCK TABLES
对表上锁,在服务层实现,和引擎无关,不能用于替代事务。
- 自动提交:
1.4 MVCC
MVCC是行级锁的一个变种,但很多情况下避免加锁操作,典型有乐观实现和悲观实现。
InnoDB实现:通过保存数据在某个时间点的快照。每行记录维护2个列——行创建时间T1,行过期/删除时间T2。此外每个事务都有一个自己的时间T(没开始一个新事务,T会递增)。
只支持RR和RC。
RR下:
-
SELECT
: 满足下面2个条件即作为结果- 只找T1 <= T的数据行
- T2要么没定义,要么T2 > T
-
INSERT
: 插入新的一行,该行的T1 = T -
DELETE
: 删除一行,该行的T2 = T -
UPDATE
: 会插入一行新记录,其T1 = T;同时把原来行的T2 = T
大多数读不用加锁,读性能好。但是要额外的存储空间,做更多的行检查工作,以及额外的维护。
1.5 MySQL存储引擎
可用
SHOW TABLE STATUS
显示表的相关信息
-
InnoDB(默认)
用来处理大量短期事务,且很少回滚。
- 数据存储在表空间(一系列数据文件组成),每个表数据和索引放在单独文件中
- 使用MVCC支持高并发,实现4个隔离标准,默认为RR
- 使用间隙锁防止幻读(锁定查询的行且对索引间隙进行锁定,防止幻行插入)
- 表基于聚簇索引建立(查询性能高),其辅助索引必须包含主键列,所以主键应尽可能小,否则索引会非常大
- 存储格式平台独立
- 内部优化:可预测读、自适应hash索引、插入缓冲区等等
- 是事务型存储引擎,支持热备份(系统处于正常运转状态下的备份,不需要停止写入)
-
MyISAM
引擎设计简单,数据以紧密格式存储,某些场景下性能很好。
不支持事务特性,不支持行锁,是以前的默认存储引擎。
- 表存储在2个文件:数据文件(
.myd
),索引文件(.myi
) - 加锁与并发:只支持表锁,读上S锁,写上X锁。不过查询同时可以插入新记录,即并发插入
- 可手动修复,但不支持事务级别的恢复
- 支持全文索引(InnoDB不支持),且可延迟更新索引键(
DELAY_KEY_WRITE
选项,极大提高写入性能,但是崩溃后索引会损坏,需要修复) - 可对表压缩,适合导入数据后不再修改的场景,其索引是只读的
- 表存储在2个文件:数据文件(
-
Memory
数据保存在内存中,性能更好,但是重启后数据会丢失(结构不会丢失)
- 只支持表锁
- 支持Hash索引
- 适用于查找/映射表,缓存周期性聚合数据,保存中间数据
1.6 选用合适的引擎
从这些因素考虑:事务,备份 ,崩溃恢复,特有特性
1.7 转换存储引擎
-
ALTER TABLE table_name ENGINE = engine_name;
-
mysqldump
导出,并修改CREATE TABLE
引擎选项,然后导入 -
CREATE
&SELECT
,综合使用前2种方法
Chapter 2:MySQL Benchmark
-
策略:集成式、单组件式
- 测试指标
- 吞吐量:单位时间处理的事务数量
- 响应时间/延迟:任务所需的整体时间(如平均、最小、最大和所占百分比)
- 并发性:关注的是正在工作中的并发操作/同时工作的线程数或者连接数。测量并发性增加时,吞吐量是否变小,响应时间是否变长。
- 可扩展性:给系统增加几倍工作量或者几倍资源,是否能获得相应几倍的结果,且其他性能在可接受范围内。
- 测试工具
- 集成式:
ab
,http_load
,JMeter
- 单组件:
mysqlslap
,MySQL Benchmark Suite
,sysbench
,Database Test Suite
- 集成式:
- 其它内容
- OLTP:联机事务处理(On-Line Transaction Processing) 主要是执行基本日常的事务处理,比如数据库记录的增删查改。比如在银行的一笔交易记录,就是一个典型的事务。
- 特点:
- 实时性要求高。我记得之前上大学的时候,银行异地汇款,要隔天才能到账,而现在是分分钟到账的节奏,说明现在银行的实时处理能力大大增强。
- 数据量不是很大,生产库上的数据量一般不会太大,而且会及时做相应的数据处理与转移。
- 交易一般是确定的,比如银行存取款的金额肯定是确定的,所以OLTP是对确定性的数据进行存取
- 高并发,并且要求满足ACID原则。比如两人同时操作一个银行卡账户,比如大型的购物网站秒杀活动时上万的QPS请求。
- 特点:
- OLAP:联机分析处理(On-Line Analytical Processing) 是数据仓库系统的主要应用,支持复杂的分析操作,侧重决策支持,并且提供直观易懂的查询结果。典型的应用就是复杂的动态的报表系统。(数据立方体)
- 特点:
- 实时性要求不是很高,比如最常见的应用就是天级更新数据,然后出对应的数据报表。
- 数据量大,因为OLAP支持的是动态查询,所以用户也许要通过将很多数据的统计后才能得到想要知道的信息,例如时间序列分析等等,所以处理的数据量很大;
- OLAP系统的重点是通过数据提供决策支持,所以查询一般都是动态,自定义的。所以在OLAP中,维度的概念特别重要。一般会将用户所有关心的维度数据,存入对应数据平台。
- 特点:
- OLTP:联机事务处理(On-Line Transaction Processing) 主要是执行基本日常的事务处理,比如数据库记录的增删查改。比如在银行的一笔交易记录,就是一个典型的事务。
Chapter 3 服务器性能剖析
3.1 服务器负载
- 捕获查询日志
- 可设置
long_query_time
捕获超过该时长的查询(即慢查询日志,而通用日志很少用) - TCP抓包,然后根据MySQL通信协议进行解析(如
tcpdump
->pt-query-digest --type=tcpdump
解析)
- 可设置
- 分析查询日志
- 生成查询日志报告(如使用
pt-query-digest
,--explain
输出执行计划,--limit
,`–outliers显示更多详细信息)
- 生成查询日志报告(如使用
3.2 单条查询
SHOW PROFILE [FOR QUERY id]
:默认禁用,可通过SET profiling = 1;
打开。可以查看对应查询不同阶段的时间SHOW STATUS
:查看MySQL服务器的一些计数器(如Querie
,Innodb_row_lock_time
等),但无法提供基于时间的统计EXPLAIN [SQL Statement]
: 查看该SQL语句的执行计划。具体的参数可参考:https://www.jianshu.com/p/ea3fc71fdc45- 慢查询日志:在Percona Server中使用
SHOW PROFILE
- Performance Schema
3.3 捕获诊断数据
-
使用诊断触发器(如
pt-stalk
) -
使用收集诊断数据工具(包含系统状态,CPU利用率,磁盘使用率和可用空间,
ps
输出采样,内存利用率),也可以从MySQL中获得信息,如SHOW STATUS
,SHOW PROCESSLIST
,SHOW INNODB STATUS
3.4 其它剖析工具
USER_STATISTIC
表strace
,oprofile
Chapter 4 Schema与数据类型
4.1 数据类型优化原则
选择更小的,选择简单的,避免NULL
- 整数
- 包含
TINYINT
,SMALLINT
,MEDIUMINT
,INT
,BIGINT
,分别使用8,16,24,32,64位存储空间 - 可配置
UNSIGNED
属性 - 可指定整数类型的宽度,如
INT(11)
,但是对存储和计算而言无意义 - 整数计算一般使用
BITINT
,即使是32-bit环境
- 包含
- 实数
- 非精确(浮点)类型:
FLOAT
,DOUBLE
;精确类型:DECIMAL
- 浮点类型使用
DOUBLE
计算 - 精确类型,CPU不支持
DECIMAL
直接运算,会更慢 - 支持指定精度,会影响列空间消耗,如
DECIMAL(18,9)
意为18位定点精度,9位小数位数
- 非精确(浮点)类型:
- 字符串
-
VARCHAR
与CHAR
(类似的有二进制的BINARY
和VARBINARY
)-
VARCHAR
: 变长字符串,比定长更省空间。前1~2字节用于记录长度。行尾空格会保留。InnoDB下过长字符串会被保存为BLOB
虽然省空间,但是当
UPDATE
时需要做额外动作,如MyISAM拆成不同片段,InnoDB分裂页。所以当字符串最大长度比平均长度长很多,且列很少更新时适用。给
VARCHAR
分配过大长度是不明智的 -
CHAR
: 定长字符串。行尾空格不保留。适合存短字符串,或者长度方差不大(如适合存储MD5值)。更新多的情况
CHAR
比VARCHAR
好,因为碎片更少。
-
-
BLOB
与TEXT
这两种类型都是为了存储大数据而设计的字符串数据类型,分别用二进制(无排序规则或字符集)和字符(有排序规则和字符集)方式存储。
TYNYBLOB
,SMALLBLOB
,BLOB
,MEDIUMBLOB
,LONGBLOB
为一个家族,BLOB == SMALLBLOB
TYNYTEXT
,SMALLTEXT
,TEXT
,MEDIUMTEXT
,LONGTEXT
为一个家族,TEXT == SMALLTEXT
MySQL把它们当做一个独立对象处理。当它过大时,InnoDB会用专门的外部存储区域来存储它们,每个值需要在行内存储1~4个指针引用它们。
排序和索引时也不是对全部长度进行操作,而只是取前面的一小部分。
所以避免使用它们,若要使用,则尽量使用
SUBSTRING(column, length)
转成字符串,就可以使用内存临时表,否则使用磁盘临时表会很耗性能(可在EXPLAIN
执行计划的Extra
列看到Using temporary
,说明使用了隐式临时表)。 -
ENUM
枚举把不重复的字符串存储为一个预定义的集合,而在列中就是一个整数,并保存一个整数到字符串的映射。因此,它的存储很紧凑,表的大小会更小,性能较好。
枚举的排序是默认按照对应的数字排序的。可利用
FIELD()
函数显式定义排序规则,但是导致无法用索引消除排序。作
JOIN
运算时,都为ENUM
会很快,但ENUM
和VARCHAR/CHAR
一起会慢很多。
-
-
日期与时间
-
DATETIME
: 范围从1001~9999年,精度为秒,格式为YYYYMMDDHHMMSS
,与时区无关,长度8字节。默认可排序的字符串格式为YYYY-MM-DD HH:MM:SS
,即ANSI标准定义的日期和时间表示方法 -
TIMESTAMP
:时间戳,同UNIX时间戳,有时区影响,长度4字节,范围比DATETIME
小的多
除了特殊行为外,通常用
TIMESTAMP
, 因为空间效率更高。 -
-
位数据
-
BIT
存储位,
BIT(n)
表示存储n个位,最长64。在MyISAM中所有的BIT
都打包存储,而InnoDB对每个列使用最小整数类型存放,所以不能节省空间。BIT
被当做字符类型而非数字类型,检索时是当成位字符串的。(而在数字上下文中会转成数字,需要注意) -
SET
内部是一系列打包的位的集合表示,有效利用存储空间。也有
FIND_IN_SET()
和FIELD()
等函数方便查询,但是定义代价高,且修改需要ALTER TABLE
, 不能在SET
上通过索引查找。可用
INT
等整数类型替代,然后进行位操作。
-
-
标识符(Identifier)
- 整数类型是最好的选择,速度快且可用
AUTO_INCREMENT
ENUM
和SER
不适合,它们适合存储固定的状态信息- 字符串不适合,很耗空间且比数字慢
- 若存储UUID,应去除
-
符号,或者用UNHEX()
函数转成16字节的数字存在BINARY(16)
中。检索时用HEX()
格式化为16进制格式 - IPv4地址应该用无符号整数存储,而非字符串。MySQL提供
INET_ATON()
和INET_NTOA()
函数进行转换
- 若存储UUID,应去除
- 整数类型是最好的选择,速度快且可用
4.2 Schema设计
MySQL中需要避免:
- 太多的列
- 太多的关联(MySQL限制为最多61个,单个查询最好在12个表以内关联)
- 全能(枚举量过多)和变相(不必要的)的枚举
- Not Invent Here的
NULL
。虽然避免用空值有很多好处,但是不能走极端,有时候使用NULL
会让代码处理更加简单
4.3 范式与反范式
-
一个表R可有多个超码,最小的超码是候选码,候选码有多个。主键是候选码的其中一个。
- 范式
- 1NF:一个表R的所有列都是原子的(不可分的)
- 2NF:
- BCNF:一个表R的所有函数依赖满足下列条件之一
- α -> β是平凡的
- α是R的一个超码
- 3NF:一个表R的所有函数依赖蛮子下列条件之一
- α -> β是平凡的
- α是R的一个超码
- β - α的每个属性A都包含于R的其中一个候选码(候选码可有多个,可包含于不同的候选码中)
- 范式的优缺点
- 优点
- 冗余小,表更小
- 执行更新(写操作)快
- 由于表小,可以更好地放入内存中,所以执行操作更快
- 需要更少的
DISTINCT
或者GROUP BY
,查询代码更加简单
- 缺点
- 会有更多的关联,而这个运算代价很昂贵,且可能会让一些索引策略无效化
- 优点
- 反范式的优缺点
- 优点
- 用冗余避免了过多的关联,而在最坏情况下(全表扫描),相比,使用关联会带来随机I/O,性能更差(相比顺序I/O)
- 更有效使用索引策略
- 缺点
- 数据冗余问题
- 没有了范式化的优点
- 优点
4.4 缓存表与汇总表
用缓存表和汇总表保存衍生的冗余数据,在容许少量不精确数据的情况下,可以提升性能(减少耗费大的实时计算)
-
缓存表与汇总表
- 缓存表:存储那些可以比较简单从其他表获取的数据的表(但获取较慢的,是逻辑上冗余的数据)
- 汇总表:存储聚合数据的表(数据不是逻辑上冗余的)
- 影子表:指一张真实表背后创建的表,在表重建的时候有用。如果要重建
table1
,那么可以先创建table1_phantom
,然后往影子表中填充好数据,填好了和真实表作切换,这样在重建的时候,原表依然可用
-
物化视图
-
视图:视图概念上包含查询结果,但不预先计算和存储,在使用这个视图的时候才会去计算。可以看成对用户可见的“虚关系”,可被用于查询。在查询语句使用视图时,视图会被它存储的查询表达式代替。
1 2
CREATE VIEW view_name(column_name, ...) AS [SELECT clause]
-
物化视图:视图原本不存储计算的结果,物化视图允许数据库存储视图关系的内容(即计算结果)。若用于定义视图的实际的表发生改变,该视图(的内容)也会被更新。
MySQL不原生支持物化视图。但是开源工具
Flexviews
实现了物化视图。 -
-
计数器表
计数器表可以用于计数,小而快,并可以帮助避免查询缓存失效。
一些设计技巧:
- 避免将表设成1行(因为记录上有全局互斥锁,并发度不高),可设成多行,然后随机选择行数更新,统计的时候使用聚合函数查询
- 可写一个周期执行任务,周期性地合并结果到某一行中,这样可以避免表变得太大
设计高性能数据库时,一种技巧是:更快地读,更慢地写。在不显著减少写性能的前提下,尽量提高读性能。
4.5 加速ALTER TABLE
MySQL执行大部分ALTER TABLE
操作是:创建新空表 -> 把旧表数据插入新表 -> 删除旧表,常导致服务中断。
常用的一些技巧:
- 在不提供服务的机器上
ALTER TABLE
, 然后和提供服务的主库进行切换 - 影子拷贝
一些操作不会引起表重建,如
只修改.frm
文件:
- 修改列默认值(
ALTER TABLE table ALTER COLUMN column ...
) - 移除一个列的
AUTO_INCREMENT
属性 - 修改
ENUM
和SET
常量
步骤常为:
- 创建一张相同空表,进行所需的更改
- 执行
FLUSH TABLES WITH READ LOCK
来关闭所有的表,并禁止任何表读取 - 交换
.frm
文件 - 执行
UNLOCK TABLES
释放第2步的锁