(注:文档参考高性能MySQL,SQL手册,官方文档
[提前声明]
文章由作者:张耀峰 结合自己生产中的使用经验整理,最终形成简单易懂的文章
写作不易,转载请注明,谢谢!
大数据代码案例地址: https://github.com/Mydreamandreality/sparkResearch
MySQL基本架构
连接器
我们要进行查询,首先肯定是要去连接数据库,这个时候我们操作的就是连接器
连接器负责跟客户端建立连接 获取权限 维持和管理连接
连接的时候会经过TCP握手 然后进行身份验证
连接成功之后 连接是处于空闲状态
查询缓存
MySQL缓存机制
MySQL查询缓存是MySQL中比较独特的一个缓存区域,用来缓存特定Query的整个结果集信息,且共享给所有客户端
为了提高完全相同的Query语句的响应速度,MySQL Server会对查询语句进行Hash计算后,把得到的hash值与Query查询的结果集对应存放在Query Cache中
当MySQL Server打开Query Cache之后,MySQL Server会对接收到的每一个SELECT 语句通过特定的Hash算法计算该Query的Hash值,然后通过该hashi值到Query Cache中去匹配
如果没有匹配,将这个hash值存放在一个hash链表中,并将Query的结果集存放到cache中,存放hash值链表的每个hash节点存放了相应Quey结果集在cache中的地址,以及该query所涉及到一些table相关信息;
如果通过hash值匹配到了一样的Query,则直接将cache中相应的Query结果集返回给客户端
缓存失效
如果表更改了,那么使用这个表的所有缓存查询将不再有效,查询缓存中值相关条目被清空。这里的更改指的是表中任何数据或是结构发生改变,包括INSERT、UPDATE、 DELETE、TRUNCATE、ALTER TABLE、DROP TABLE或DROP DATABASE等
内存管理
MySQL Query Cache 使用内存池技术,自己管理内存释放和分配,而不是通过操作系统。内存池使用的基本单位是变长的block, 用来存储类型、大小、数据等信息;一个result set的cache通过链表把这些block串起来。block最短长度为query_cache_min_res_unit
使用
其实不建议在生产中使用MySQL Cache
查询语句的hash计算和hash查找带来的资源消耗。如果将query_cache_type设置为1(也就是ON),那么MySQL会对每条接收到的SELECT类型的查询进行hash计算,然后查找这个查询的缓存结果是否存在。虽然hash计算和查找的效率已经足够高了,一条查询语句所带来的开销可以忽略,但一旦涉及到高并发,有成千上万条查询语句时,hash计算和查找所带来的开销就必须重视了
Query Cache的失效问题。如果表的变更比较频繁,则会造成Query Cache的失效率非常高。表的变更不仅仅指表中的数据发生变化,还包括表结构或者索引的任何变化。查询语句不同,但查询结果相同的查询都会被缓存,这样便会造成内存资源的过度消耗。查询语句的字符大小写、空格或者注释的不同,Query Cache都会认为是不同的查询(因为他们的hash值会不同)
相关系统变量设置不合理会造成大量的内存碎片,这样便会导致Query Cache频繁清理内存
开发中不应该使用MySQL Cache 建议手动关闭
使用缓存中间件来代替MySQL Cache
分析器
其处理过程:词法分析,语法分析,语义分析
优化器
经过分析器后,MySQL知道SQL是做什么操作的,优化器的作用是,当一个表存在多个索引的时,决定使用哪个索引;或者一个语句存在多表关联(join)时,决定各个表的连接顺序
执行器
到了这一步,MySQL才会开始验证用户是否有查询权限,有才开始执行,没有则返回提示没有权限,理论上这一步很快,除非设置了过多的权限
MySQL中的并发控制
此处的并发控制指的是服务器与存储引擎两个层面的控制
场景:一张表,一条数据
同时写入
同时读:无任何影响,因为读不修改数据,但此时我在读取该数据,你在删除该数据
解决以上问题的方法:并发控制:
锁的概念:
注:在Mysql中,每时每刻都发生锁定,当某个用户在修改一部分数据时,MySQL会通过锁定防止其他用户读取同一数据,大多数时候,MySQL锁的内部管理都是透明的
提高共享资源并发性的方式就是让锁定对象更优选择性,尽量只锁定需要修改的数据,而不是所有资源,任何时候,给定的资源中,锁定的数据量越少,系统的并发程度越高
注:加锁也需要消耗资源,获得锁,检查锁状态,释放等,都会增加系统开销,如果系统花费大量的时间来管理锁,而不是存取数据,那么系统数据可能会受到影响
所谓的锁策略,就是在锁的开销和数据的安全性之间寻求平衡,这种平衡当然会影响到性能
MySQL提供多种选择,不同的存储引擎都可以实现自己的所策略和锁粒度
MySQL中两个重要的锁策略:
表锁
行级锁
注:锁只在存储引擎层实现,MySQL服务器层没有实现,并且完全不了解存储引擎中锁实现
注:这些是数据库的通用基础
事务是一组原子性的SQL查询,独立的工作单元
如果能成功的执行工作单元内的全部语句,那么就执行,否则的话,如果有任何一条语句因为崩溃或者其它问题无法执行,那么工作单元内的所有语句都不会执行
简单来说,要么全部执行成功,要么全部执行失败
事务ACID
A:atomicity 原子性
C:consistency 一致性
I:isolation 隔离性
D:durability 持久性
原子性:
一致性:
隔离性:
持久性:
查看系统当前隔离级别:select @@global.tx_isolation
设置系统当前隔离级别:set global transaction isolation level repeatable read
什么是隔离级别:正常来讲,MySQL中的一次操作过程我们可以定义为一次事务,如果多个线程同时操作MySQL中的数据(同一条,或者同一个范围内的)就叫并发事务,对并发事务进行操作哦粒度的控制就是隔离级别
在SQL的标准中定义了四种隔离级别,每一种级别都规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的,较低级别的隔离通常可以执行更高的并发,系统的开销也更低
注:每种存储引擎实现的隔离级别不一定相同
说白了隔离级别的核心就是不同的锁粒度,配合锁粒度来理解
可能产生的疑问:uncommitted不是有排它锁?并且是在事务完成后再去释放的,为什么其它线程可以读取到未提交的数据,这个不符合排它锁的特性
浅析:uncommitted的操作是对事务内的每一条更新语句对应的行记录加上写锁来操作,而不是把一个事务整体来加锁,所以它只能保证写的排它性
可能产生的疑问:既然不是对事务整体加锁,如何保证在事务提交前其它事务不可见
浅析:使用MVCC技术(一种非阻塞的读操作)MVCC的实现,是通过保存数据在某个时间点的快照来实现的。即为:不管需要执行多长时间,每个事务看到的数据都是一致的
注:MVCC是引擎实现的 不同的引擎实现不同 MVCC只会在RC 和 RR两个隔离级别下工作
REPEATABLE READ(可重复读)(MySQL默认级别)
SERIALIZABLE(可串行化)
死锁是指两个或者多个事务在同一资源上互相占用,请求锁定对方占用的资源,从而导致恶性循环的现象,当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁
案例:
事务A:
update ... where id =1
update ... where id =2
事务B:
update ... where id =2
update ... where id =1
如果凑巧,两条事务都执行了第一条更新语句,同时锁定当前数据,接着两个事务都尝试去执行第二条语句,却发现被锁定,然后两个事务都等待对方释放锁,同时又持有对方需要的锁,陷入死循环,此时除非有外部因素介入才可能解除死锁
为了解决这种问题,数据库实现了各种死锁检测和死锁超时机制
解决方案:
当查询的时间达到锁等待超时的设定后放弃锁请求
InnDB处理死锁的方法是:将持有最少行级排它锁的事务进行回滚,就能避免死锁
注:死锁发生后,只有部分或者完全回滚其中一个事务,才能打破死锁,对于事务性的系统,这是无法避免的
事务日志可以提交事务的效率,使用事务日志,在修改表的数据时,只需要修改内存拷贝,再把修改行为记录到持久在硬盘上的事务日志中(binlog),而不用每次豆浆修改的数据本身持久到磁盘,事务日志采用的是追加的方式,因此写入日志的操作是磁盘上的一小块区域内的顺序IO,不是随机IP需要在磁盘的多个地方移动磁头,所以采用事务日志的方式相对来说要快得多,事务日志持久之后,内存中被修改的数据在后台可以慢慢的刷回到磁盘,(预写式日志),修改数据需要写两次磁盘
如果数据的修改意见记录到日志并持久化,但数据本身还没有写回磁盘,这个时候系统崩溃了,存储引擎在重启时,能够自动恢复这部分修改的数据,具体恢复方式视存储引擎而定
innodb是MySQL中的事务性存储引擎
默认情况下MySQL是采用自动提交的模式,就是每一个查询不需要显示的开启事务,可以自行启动或者禁用自动提交
SET AUTOCOMMIT = 1
隐式和显式锁定
Inndb里采用的是两阶段锁定协议,在事务的执行过程中,随时都可以执行锁定,锁只有在commit或者roolback的时候才会被释放
显式锁定可以使用
SELECT * FROM xxx FOR UPDATE
优化点:显式的LOCK table和事务本身相互影响的话,会产生一些玄学的Bug,不仅没有必要(innerdb 本身就有行级锁),而且某些情况下会非常影响性能,所以尽量避免使用显式锁定,如果有必要可以使用Redis或者zookeeper等实现锁机制
MySQL中的物理设计
第一范式:
确保数据表中每列(字段)的原子性,不可再拆分
第二范式:
在第一范式的基础上更进一步,目标是确保表中的每列都和主键相关
第三范式:
在第二范式的基础上更进一步,目标是确保表中的列都和主键直接相关,而不是间接相关
反范式:
通过增加冗余或重复的数据来提高数据库的读性能,比如中间表中增加一些需要join才能取到的数据,减少join次数,提高读能力
在范式的数据库中,每个数据出现并且只出现一次,相反反范式的数据库中,数据出现并且会出现多次,会有冗余的情况
注:良好的设计是高性能的基石,需要考虑的因素有很多,需要综合考虑,不过一般我们的设计都是混着用
在MySQL中支持的数据类型太多了,如何选择正确的数据类型对我们应用程序的性能提升是比较重要的,不管什么数据类型,都可以参考几个原则来帮助我们设计
挑几个常用的具体的讲:
int(5)和int(15)有什么区别
注:没有区别,它只是限制了显示的长度,存储的范围限制还有int的最大限制
有符号和无符号的类型都使用了相同的存储空间,性能也是一样的
在涉及到金额的时候如果对精度要求不高的情况下可以优先使用float,其次是使用double来进存储
float占4个字节,double占8个字节,
如果对精度要求比较高的情况下最好使用decimal来存储,但是相应的它的效率没有float和double那么高效,因为额外占用了空间和计算开销
varchar(100)和varchar(255)有什么区别
注:理论上没区别(只有对字符的长度限制)varchar比定长类型更节省空间,因为它只使用了必要的空间,它虽然设置了长度值,但由于是可变的,所以当存储的长度小于x的时候,它的实际存储不是x,而是字符长度+长度标识(这个一般只占1-2个字节)
正是因为varchar节省了存储空间,但是由于是可变长度,当我们更新的时候,现有的长度可能会变化,所以引擎会有一些额外的工作,比如我们更新的长度变长了,引擎需要分裂页再合并(这个过程是有排他锁的机制存在)
注:虽然varchar(100)和(19999)存储Hello world
时空间开销是一样的,但是varchar(19999)会消耗更多的内存,MySQL的内存分配机制,一般会分配固定大小的内存块来保存内部值,所以最好的策略是只分配需要的空间
char(10)的长度是固定的 如果存储的是HELLO
char在存储是会在后面追加空格,并且在检索的时候消除空格
正是由于长度是固定的,所以处理速度是比varchar要快的,但是浪费了存储空间,如果存储的数据不大,速度也有要求的话,可以考虑char类型,否则尽量别用
blob和text都是用来存储大字符的类型,blob是采用二进制存储,text是字符存储
b和t引擎会独立处理这两个类型,当它们的值太大时,引擎会使用独立的存储区域进行存储,值有1-4个字节的指针指向存储区域
在排序的处理上也与其他类型不同,只针对最前面的max_sort_length自己进行排序,如果只需要对前面的更少的字节进行排序,那么可以通过设置max_sort_length参数或者substring(value,length)来截取部分字符串
在实际使用中应该慎用这两个类型,尤其是会创建临时表的情况下,因为如果临时表大小超过max_heap_table_size或者tmp_table_size,就会将临时表存储在磁盘上,进而导致整体速度下降
MySQL中时间类型可选择性有很多,year,date…等,但它们都有自己的场景,t和d是两种很相似的日期类型,一般情况下,它们都可以实现同样的功能,但是一些场景下,timestamp做的更好
timestamp和datetime都可以以ANSI标准日期显示
datetime能保存1001-9999年 精度为秒的数据到整数中YYYYMMDDHHMMSS,时间与时区无关,使用8个字节的存储空间
timestamp保存了1970/1/1以来的秒数,它只使用4个字节的存储空间,所以它的时间范围比datetime小,只表示1970-2038年,时间是依赖于时区的
注:一般情况下,可以尽量使用timestamp,因为它的空间效率更高
合理的利用索引
索引优化是对查询性能优化最有效的有段之一,索引能够轻易的将查询性能提升几个量级,一般最优索引的设计都经常意味着这个SQL也要跟着改写
在innodb引擎中,总共有四种索引类型,两种索引方法
explain
命令来查看SQL执行计划索引可以让服务器快速地定位到表的指定位置。但是这并不是索引的唯一作用,根据创建索引的数据结构不同,索引也有一些其他的附加作用
注:索引某些情况下不是最好的解决方案,首先数据量小的情况下有索引和没索引没有区别,如果数据量在TB级别,索引的作用也有可能体现不出来,HBASE的分区策略和元数据meta表可以了解一下
不同的场景下索引的策略也不同,下面列几个常用的场景来设计索引策略
全时bugfeel平台,扫描任务模块,数据量大的情况下每次加载都需要10s+的时间,检查数据库后发现每个字段都设置了独立的索引,一般在网上查找索引如何设置时,总有把WHERE条件里面的列都建上索引
,这样模糊的建议,实际上这个建议是非常错误的,在多个列上建立独立的单列索引大部分情况下并不能提高MySQL的査询性能,MySQL在5.x版本引入了索引合并(多列索引)的策略
注:当查询出现对多个索引相交操作时(AND),那就意味着需要一个所有相关列的多列索引,而不是多个独立的索引(bugfeel扫描任务模块,通过设置正确的索引,查询时间控制在了200ms内)
联合索引的优势:
在mysql建立联合索引时会遵循最左前缀匹配的原则,即最左优先,在检索数据时从联合索引的最左边开始匹配
KEY in_col1,col2,col3 on key_test(col1,col2,col3)
SELECT * FROM tablename where col1=x and col2=y and col4=z
以上就是遵循了最左前缀匹配的原则
注:如何选择索引的列顺序有一个基本准则,将选择性最高的列放到索引的最前列,这个在某些场景下可能有用,在不考虑排序和分组时,索引只是优化where条件的查找,这种情况将选择性最高的列放在前面一般来说是很好的,能最快的过滤出需要的行
只要在联合索引中包含的字段,字段顺序是任意的,优化器会纠正
where col1 and col2
where col2 and col1
索引的性能并不只是依赖于所有索引列的选择性,也和查询条件的具体值有关(结果分布)
select * from tablename where col1 = x and col2 = y
正常来讲,我们的索引设计都是 ( col1,col2 )
但是结果的分布是
col1命中30条
col2命中10000条
这个时候按照之前的方式来优化,可能对其他一些条件值的查询不公平,服务器的整体性能可能会降低
总:所以在索引优化时,要考虑到where的排序,分组和范围条件等等情况,某一个特殊的情况就可能会对性能造成比较大的影响
索引另一个能提高性能的点在于可以让查询锁定更少的行,如果我们的查询从不访问哪些不需要的行,那么引擎就会锁定更少的行,这两个方面一结合,性能就会更好
查询语句的优化
MySQL客户端和服务器之间的通信协议是半双工协议,所以在任何一个时刻,要么是由服务器向客户端发送数据,要么是由客户端向服务器发送数据,这两个动作不能同时发生
客户端用一个单独的数据包将查询传给服务器,参数max_allowed_packet就是用来配置服务端接收的包大小,只要客户端发送了请求,能做的事情就只是等待结果了
服务端是相反的,一般服务器响应给用户的数据通常很多,由多个数据包组成,当服务器响应客户端请求时,客户端必须完整地接收整个返回结果,而不能简单地只取前面几条结果,然后让服务器停止发送数据
查询不需要的记录会给MySQL服务器带来额外的负担,并增加网络开销,消耗应用服务器的CPU和内存资源
注:典型的案例,判断数据是否存在,不需要使用count(*)来校验,直接limit 1 来校验即可
如果不是真的需要返回全部的列。SELECT * 尽量避免,使用*,会让优化器无法完成索引覆盖扫描这类优化,还会带来额外的IO,内存和cpu消耗,在高并发的情况下,这个性能的差异才会有体现
不断的重置执行相同的查询,然后每次都返回相同的数据
注:这个不算查询语句层的优化)比较好的方案就是缓存,将不会经常变动的数据存入到缓存中,减少数据库的压力,并且为了避免缓存击穿和热点缓存的问题,可以提前指定相应策略
分而治之,将大查询切为小查询,每个查询的功能完全一样,只完成一小部分,每次只返回一小部分查询结果,把原本一次性的压力分散到一个很长的
时间段中,减少服务器的压力
注:
rows_count = xxx
do {
sql.execute("delete from xxx where xxx limit 1000")
} while rows_count > 0
count()可以统计某个列值的数量,也可以统计行数,在使用count(*)时,通配符并不会像很多地方说的扩展成所有的列,官方的描述中,*会直接忽略所有的列,去直接统计所有的行数
如果我们要统计集的行数,用count(*)定义清晰,性能也会好
尽可能的使用关联查询代替(因为一些特定的情况下,子查询的优化器会抽风,导致全表扫描的操作),最好在sql执行前做一波explain分析
关联查询要确保on或者using子句中的列上有索引,最好能确保order by 或者group by 中的表达式只涉及到一个表中的列,MySQL才有可能使用索引来优化这个过程
可扩展的MySQL
注:水平扩展的一种方案
当MySQL的规模越来越大的时候,单台服务器实例迟早会有性能瓶颈,所以才需要水平扩展(集群)
集群的优势:
MySQL主从复制是官方提供的一种解决方案
核心:
主从复制是通过重放binlog实现主库数据的异步复制。即当主库执行了一条sql命令,那么在从库同样的执行一遍,从而达到主从复制的效果。在这个过程中,master对数据的写操作记入二进制日志文件中(binlog),生成一个 log dump线程,用来给从库的 i/o线程传binlog。而从库的i/o线程去请求主库的binlog,并将得到的binlog日志写到中继日志(relaylog)中,从库的sql线程,会读取relaylog文件中的日志,并解析成具体操作,通过主从的操作一致,而达到最终数据一致
特性:
MySQL主从的结构,可以实现数据的多点备份,但是没有故障自动转移和负载均衡,对比单台服务器实例,主从的优势
优势:
劣势:
在MySQL主从的基础上增加了故障监测与转移,自动数据分片功能,不过还是一主多从的结构,当主节点挂了,会在从节点中选举一个master节点
注:这个性能太差,几乎没有人用
mysql集群(MySQL Cluster)也是mysql官方提供的
MySQL Cluster是多主多从结构
优势:
注:由于架构模式和原理太复杂,维护成本太高,且节点与节点之间通信太多,对带宽要求太高,所以也几乎没有什么人用
MMM(Master Replication Manager for MySQL)
是一套支持双主故障切换和双主日常管理的脚本程序,双主多从结构,这是Google的开源项目,使用Perl语言来对MySQL Replication做扩展,提供一套支持双主故障切换和双主日常管理的脚本程序,主要用来监控mysql主主复制并做失败转移,高可用性,扩展性好
注:MMM的双主实际上只有一台主在工作,并且实时同步到另一台主几点,当master1挂掉后自动切换到master2节点
MHA(Master High Availability Manager and Toolsfor MySQL)目前在Mysql高可用方面是一个相对成熟的解决方案
它是日本的一位MySQL专家采用Perl语言编写的一个脚本管理工具,该工具仅适用于MySQLReplication 环境,目的在于维持Master主库的高可用性。
MHA是基于标准的MySQL复制(异步/半同步)。MHA是由管理节点(MHA Manager)和数据节点(MHA Node)两部分组成。MHA Manager可以单独部署在一台独立机器,也可以部署在一台slave上
读写分离可以从两个方面解决,一个是客户端另一个是中间件
TDDL、 Sharding-Jdbc (常用shardding-jdbc)
通过这种方式来实现读写分离优势不多,并且缺点很致命
首先说优点,由我们自己来控制程序逻辑,不需要额外的中间件来维护,并且理论上支持任意的数据库,数据源也方便管理,但额外也引入了高昂的开发成本,代码入侵性,主要是做不到动态的数据源,并且这个过程运维是没有办法介入的,所以在大型项目开发中尽量少用
mysql proxy、mycat、altas (常用mycat)
通过中间件的方式避免了代码的入侵性,并且可以实现动态的数据源,但是也额外引入了维护中间件的工作,由于中间件就是一层代理,所以性能方面也会有一定的损耗,但如果是多从方案,其实影响不大,性能能得到保证,但是中间件proxy也有可能单点故障,所以需要做成HA的架构