作者:京东衰弱 孟飞
1、 数据库性能优化的意义
业务倒退初期,数据库中量个别都不高,也不太容易出一些性能问题或者出的问题也不大,然而当数据库的量级达到肯定规模之后,如果缺失无效的预警、监控、解决等伎俩则会对用户的应用体验造成影响,重大的则会间接导致订单、金额间接受损,因此就须要时刻关注数据库的性能问题。
2、 性能优化的几个常见措施
数据库性能优化的常见伎俩有很多,比方增加索引、分库分表、优化连接池等,具体如下:
序号 | 类型 | 措施 | 阐明 |
---|---|---|---|
1 | 物理级别 | 晋升硬件性能 | 将数据库装置到更高配置的服务器上会有空谷传声的成果,例如进步CPU配置、减少内存容量、采纳固态硬盘等伎俩,在经费容许的范畴能够尝试。 |
2 | 利用级别 | 连接池参数优化 | 咱们大部分的利用都是应用连接池来托管数据库的连贯,然而大部分都是默认的配置,因此配置好超时时长、连接池容量等参数就显得尤为重要。 1、 如果链接长时间被占用,新的申请无奈获取到新的连贯,就会影响到业务。 2、 如果连接数设置的过小,那么即便硬件资源没问题,也无奈施展其效用。之前公司做过一些压测,但就是死活不达标,最初发现是因为连接数太小。 |
3 | 单表级别 | 正当使用索引 | 如果数据量较大,然而又没有适合的索引,就会拖垮整个性能,然而索引是把双刃剑,并不是说索引越多越好,而是要依据业务的须要进行适当的增加和应用。 缺失索引、反复索引、冗余索引、失控索引这几类状况其实都是对系统很大的危害。 |
4 | 库表级别 | 分库分表 | 当数据量较大的时候,只应用索引就意义不大了,须要做好分库分表的操作,正当的利用好分区键,例如依照用户ID、订单ID、日期等维度进行分区,能够缩小扫描范畴。 |
5 | 监控级别 | 增强运维 | 针对线上的一些零碎还须要进一步的增强监控,比方订阅一些慢SQL日志,找到比拟蹩脚的一些SQL,也能够利用业务内一些通用的工具,例如druid组件等。 |
3、 MySQL底层架构
首先理解一下数据的底层架构,也有助于咱们做更好优化。
一次查问申请的执行过程
咱们重点关注第二局部和第三局部,第二局部其实就是Server层,这层次要就是负责查问优化,制订出一些执行打算,而后调用存储引擎给咱们提供的各种底层根底API,最终将数据返回给客户端。
4、MySQL索引构建过程
目前比拟罕用的是InnoDB存储引擎,本文探讨也是基于InnoDB引擎。咱们始终说的加索引,那到底什么是索引、索引又是如何造成的呢、索引又如何利用呢?这个话题其实很大也很小,说大是因为他底层的确很简单,说小是因为在大部分场景下程序员只须要增加索引就好,不太须要理解太底层原理,然而如果理解不透彻就会引发线上问题,因此本文均衡了大家的了解老本和常识深度,有肯定底层原理介绍,然而又不会太过深刻导致难以了解。
首先来做个试验:
创立一个表,目前是只有一个主键索引
CREATE TABLE t1
(
a int NOT NULL,
b int DEFAULT NULL,
c int DEFAULT NULL,
d int DEFAULT NULL,
e varchar(20) DEFAULT NULL,
PRIMARYKEY(a)
)ENGINE=InnoDB
插入一些数据:
insert into test.t1 values(4,3,1,1,'d');
insert into test.t1 values(1,1,1,1,'a');
insert into test.t1 values(8,8,8,8,'h');
insert into test.t1 values(2,2,2,2,'b');
insert into test.t1 values(5,2,3,5,'e');
insert into test.t1 values(3,3,2,2,'c');
insert into test.t1 values(7,4,5,5,'g');
insert into test.t1 values(6,6,4,4,'f');
MYSQL从磁盘读取数据到内存是依照一页读取的,一页默认是16K,而一页的格局大略如下。
每一页都包含了这么几个内容,首先是页头、其次是页目录、还有用户数据区域。
1)方才插入的几条数据就是放到这个用户数据区域的,这个是依照主键顺次递增的单向链表。
2)页目录这个是用来指向具体的用户数据区域,因为当用户数据区域的数据变多的时候也就会造成分组,而页目录就会指向不同的分组,利用二分查找能够疾速的定位数据。
当数据质变多的时候,那么这一页就装不下这么多数据,就要决裂页,而每页之间都会双向链接,最终造成一个双向链表。
页内的单向链表是为了查找快捷,而页间的双向链表是为了在做范畴查问的时候提效,下图为示意图,其中其二页和第三页是复制的第一页,并不实在。
而如果数据还持续累加,光这几个页也不够了,那就逐渐的造成了一棵树,也就是说索引B-Tree是随着数据的积攒逐渐构建进去的。
最下边的一层叫做叶子节点,上边的叫做内节点,而叶子节点中存储的是全量数据,这样的树就是聚簇索引。始终有同学的了解是说索引是独自一份而数据是一份,其实MySQL中有一个准则就是数据即索引、索引即数据,实在的数据自身就是存储在聚簇索引中的,所谓的回表就是回的聚簇索引。
然而咱们也不肯定每次都依照主键来执行SQL语句,大部分状况下都是依照一些业务字段来,那就会造成别的索引树,例如,如果依照b,c,d来创立的索引就会长这样。
举荐1个网站,能够可视化的查看一些算法原型:
目录:
https://www.cs.usfca.edu/~gal...
B+树
https://www.cs.usfca.edu/~gal...
而在MySQL官网上介绍的索引的叶子节点是双向链表。
对于索引构造的小结:
对于B-Tree而言,叶子节点是没有链接的,而B+Tree索引是单向链表,然而MySQL在B+Tree的根底之上加以改进,造成了双向链表,双向的益处是在解决> <,between and等'范畴查问'语法时能够得心应手。
5、MySQL索引的一些应用标准
1、 只为用于搜寻、排序或分组的列创立索引。
重点关注where语句后边的状况
2、 当列中不反复值的个数在总记录条数中的占比很大时,才为列建设索引。
例如手机号、用户ID、班级等,然而比方一张全校学生表,每条记录是一名学生,where语句是查问所有’某学校‘的学生,那么其实也不会进步性能。
3、 索引列的类型尽量小。
无论是主键还是索引列都尽量抉择小的,如果很大则会占据很大的索引空间。
4、 能够只为索引列前缀创立索引,缩小索引占用的存储空间。
alter table single_table add index idx_key1(key1(10))
5、 尽量应用笼罩索引进行查问,以防止回表操作带来的性能损耗。
select key1 from single_table order by key1
6、 为了尽可能的少的让聚簇索引产生页面决裂的状况,倡议让主键自增。
7、 定位并删除表中的冗余和反复索引。
冗余索引:
单列索引:(字段1)
联结索引:(字段1 字段2)
反复索引:
在一个字段上增加了一般索引、惟一索引、主键等多个索引
6、 执行打算
其中罕用的是:
possible_keys: 可能用到的索引
key: 理论应用的索引
rows:预估的须要读取的记录条数
7、 线上案例
案例1:
在建设互联网医院零碎中,问诊单表过后量级23万左右,其中有一个business_id字符串字段,这个字段用来记录内部订单的ID,并且在该字段上也加了索引,然而'依据该ID查问详情'的SQL语句却总是时好时坏,性能不稳固,快则10ms,慢则2秒左右,SQL大体如下:
select 字段1、字段2、字段3 from nethp_diag where business_Id = ?
因为business_id是记录第三方零碎的订单ID,为了兼容不同的第三方零碎,因此设计成了字符串类型,但如果传入的是一个数字类型是无奈应用索引的,因为MySQL只能将字符串转数字,而不能将数字转字符串,因为内部的ID有的是数字有的是字符串,因此导致索引一会能够走到,一会走不到,最终导致了性能的不稳固。
案例2:
在某次大促的当天,忽然接到DBA运维的报警,说数据库忽然流量激增,CPU也打到100%了,影响了局部线上性能和体验,遇到这种状况过后大部分人都比拟缓和,下图为过后的数据库流量状况:
相干SQL语句:
<!--统计医患下过来24小时内开的电子病历总数-->
<select id="getCountByDPAndTime" resultType="integer">
select count(1)
from jdhe_medical_record
where status = 1 and is_test = #{isTest,jdbcType=INTEGER} and electric_medical_record_status in (2,3)
<if test="patientId != null">
and patient_id = #{patientId,jdbcType=BIGINT}
</if>
<if test="doctorPin != null">
and doctor_pin = #{doctorPin,jdbcType=VARCHAR}
</if>
and created >#{dateStart,jdbcType=TIMESTAMP};
</select>
过后的索引状况
过后的执行打算
其实在patientId和doctor_pin两个字段上是有索引的,然而因为线上状况的扭转,导致test判断没有进入,这样的通用查问导致这两个字段没有设置上,进而导致了数据库扫描的量激增,对数据库产生了很大压力。
案例3:
2020年某日上午收到数据库CPU异样报警,对线上有肯定的影响,后续查看数据库CPU状况如下,从7点51分开始,CPU从8%霎时达到99.92%,丝毫没有给程序员留任何情面。
过后的SQL语句:
select rx_id, rx_create_time from nethp_rx_info where rx_status = 5 and status = 1 and rx_product_type = 0 and (parent_rx_id = 0 or parent_rx_id is null) and business_type != 7 and vender_id = 8888 order by rx_create_time asc limit 1;
过后的索引状况:
PRIMARY KEY (id
), UNIQUE KEY uniq_rx_id
(rx_id
), KEY idx_diag_id
(diag_id
), KEY idx_doctor_pin
(doctor_pin
) USING BTREE, KEY idx_rx_storeId
(store_id
), KEY idx_parent_rx_id
(parent_rx_id
) USING BTREE, KEY idx_rx_status
(rx_status
) USING BTREE, KEY idx_doctor_status_type
(doctor_pin
, rx_status
, rx_type
), KEY idx_business_store
(business_type
, store_id
), KEY idx_doctor_pin_patientid
(patient_id
, doctor_pin
) USING BTREE, KEY idx_rx_create_time
(rx_create_time
)
过后这张表量级2000多万,而当这条慢SQL执行较少的时候,数据库的CPU也就下来了,复原到了49.91%,根本能够复原线上业务,从而表象就是线上间歇性的一会能够开方一会不能够,这条SQL过后总共执行了230次,过后的CPU状况也是忽高忽低,随同这条SQL语句的执行状况,从而最终证实CPU的飙升是因为这条慢SQL。当线上业务逻辑简单的时候,你很难第一工夫晓得到底是因为那条SQL引起的,这个就须要对业务十分相熟,对SQL很相熟,否则就会白白浪费大量的排查工夫。
最初的排查后果:
在头天早晨的时候增加了一条索引rx_create_time,过后没事,然而第二天却出了事变。
加索引前后走的索引不同,一个是走的rx_status(处方审核状态)单列索引,一个是走的rx_create_time(处方提交事件)单列索引,这个就要回到业务,因为处方状态是个枚举,且枚举范畴不到10个,也就说线上29,000,000的数据量也就是被分成了不到10份,rx_status=5的值是其中一份,因此通过这个索引就能够命中很多行,这是业务规定,再套用MySQL的个性,次要是以下几条:
1、没加新索引rx_create_time的时候,因为order by后边没有索引,就看where条件中是否有适合的索引,查问选择器选定rx_status这个单列索引,而rx_status=5这个条件下限度的数据行在索引中是间断,即便须要的rx_id不在索引中,再回主键聚簇索引也来得及,因为order by后边没有索引,所以走磁盘级别的排序filesort,顶峰积压的时候处方就1万到2万,跑到了100ms,白天低谷的时候几百单也就20ms。
2、新加索引之后,就分两种状况:
2.1、加索引是在早晨,以后命中的行数比拟少,因为当天早晨的时候待审核的处方的确很少,也就是rx_status=5的的确很少,查问优化器感觉反正没多少行,排序不重要,因此就还是抉择rx_status索引。
2.2、第二天白天,待审核的处方数量很多了(rx_status=5的数据量多了),过后能够命中几万数据,如果以后命中的行数比拟多,查问优化器就开始算老本,感觉排序的老本会更高,那就优先保排序吧,所以就抉择rx_create_time这个字段,然而这个索引树上没有别的索引字段的信息,没方法,简直每条数据都要回表,进而引发了劫难。
8、 举荐用书
这本书以一种滑稽风趣的格调写了MySQL的一些运行机制,非常适合浏览,了解老本大幅升高。
https://item.jd.com/13009316....
https://item.jd.com/100661819...
9、一些感悟
对于数据库的性能优化其实是一个很简单的大课题,很难通过一篇帖子讲的很全面和粗浅,这也就是为什么我的题目是‘浅析’,程序员的成长肯定是要付出代价和老本,因为只有真的在一线切身体会到过后的缓和和压力,对于一件事件能力印象粗浅,但反之也不能太过于强调代价,如果能够通过一些他人的分享就能够躲避一些本人业务的问题和谬误的代价也是好的。