Mysql慢查询优化小记

1.背景

当数据库中表的数据达到必定级别后,就须要考虑解决方案。事实上MySQL单表能够存储10亿级数据,只是这时候性能比较差,业界公认MySQL单表容量在1KW如下是最佳状态,由于这时它的BTREE索引树高在3~5之间。既然一张表没法搞定,那么就想办法将数据放到多个地方,目前比较广泛的方案可能有下列几种:java

  • 归档
  • 分库分表
  • NoSQL/NewSQL

归档适用场景:最新的数据会被常用,旧数据不多被使用。mysql

为何不用NoSQL/NewSQLsql

首先,为何不选择第三种方案NoSQL/NewSQL,RDBMS主要有如下几个优势:数据库

  • RDBMS生态完善;
  • RDBMS绝对稳定;
  • RDBMS的事务特性;
NoSQL/NewSQL做为新生儿,在咱们把可靠性当作首要考察对象时,它是没法与RDBMS相提并论的。RDBMS发展几十年,只要有软件的地方,它都是核心存储的首选。
目前绝大部分公司的核心数据都是:**以RDBMS存储为主,NoSQL/NewSQL存储为辅**!
目前互联网行业处理海量数据的通用方法:**分库分表**。

2.关于数据库中间件及分库分表

image.png

典型的数据库中间件设计方案有2种:proxy、smart-client。下图演示了这两种方案的架构apache

功能点 代理模式 客户端模式
代理方式 服务端代理(proxy:代理数据库)中: 咱们独立部署一个代理服务,这个代理服务背后管理多个数据库实例。而在应用中,咱们经过一个普通的数据源(c3p0、druid、dbcp等)与代理服务器创建链接,全部的sql操做语句都是发送给这个代理,由这个代理去操做底层数据库,获得结果并返回给应用。在这种方案下,分库分表和读写分离的逻辑对开发人员是彻底透明的。 客户端代理(datasource:代理数据源): 应用程序须要使用一个特定的数据源,其做用是代理,内部管理了多个普通的数据源(c3p0、druid、dbcp等),每一个普通数据源各自与不一样的库创建链接。应用程序产生的sql交给数据源代理进行处理,数据源内部对sql进行必要的操做,如sql改写等,而后交给各个普通的数据源去执行,将获得的结果进行合并,返回给应用。数据源代理一般也实现了JDBC规范定义的API,所以可以直接与orm框架整合。在这种方案下,用户的代码须要修改,使用这个代理的数据源,而不是直接使用c3p0、druid、dbcp这样的链接池。
实现区别 复写MySql协议 给予JDBC扩展,以Jar包形式提供轻量级服务
优势 一、支持多语言,以mysql为例,若是proxy实现了mysql通讯协议,能够将其堪称一个mysql服务器 二、对业务透明 一、实现简单 2自然去中心化
缺点 一、实现复杂:须要实现被代理的数据库server端的通讯协议,也许只能代理某一种数据库,如mysql 二、proxy自己须要保证高可用:不能挂 三、租户隔离:多个应用都访问proxy代理的底层数据库时 一、一般仅支持某一种语言:例如tddl需使用java语言开发 二、版本升级困难:多个应用都依赖某版本jar包,有bug都要升级;而proxy只要升级代理服务器
业界实现的产品 一、阿里开源cobar 二、阿里云drds 三、奇虎360在mysql-proxy基础上开发的atlas 一、 阿里开源tddl 二、大众点评开源zebra 三、蚂蚁金服zal
image.png

3读写分离核心要点

2.1.1 sql类型判断
  • write语句:insert、update、delete、create、alter、truncate…
  • query语句:select、show、desc、explain…
2.1.2 强制走主库

具体实现上有2种方案:hint 或APIapi

  • hint:好比/master/select * from table_xx
  • Api:数据库中间件决定,ForceMasterHelper.forceMaster() //…执行多条sqlForceMasterHelper.clear()

2.2 从库路由策略

一些简单的选择策略包括:服务器

  • 随机选择(random)
  • 按照权重进行选择(weight)
  • 或者轮循(round-robin)
  • 等等
  • 就近路由:跨IDC(Internet Data Center)部署的数据库集群

分库分表

数据库中间件主要对应用屏蔽了如下过程:架构

  • sql解析:首先对sql进行解析,获得抽象语法树,从语法树中获得一些关键sql信息
  • sql路由:sql路由包括库路由和表路由。库路由用于肯定这条记录应该操做哪一个分库,表路由用于肯定这条记录应该操做哪一个分表。
  • sql改写:将sql改写成正确的执行方式。例如,对于一个批量插入sql,同时插入4条记录。但实际上用户但愿4个记录分表存储到一个分表中,那么就要对sql进行改写成4条sql,每一个sql都只能插入1条记录。
  • sql执行:一条sql通过改写后可能变成了多条sql,为了提高效率应该并发的去执行,而不是按照顺序逐一执行
  • 结果集合并:每一个sql执行以后,都会有一个执行结果,咱们须要对分库分表的结果集进行合并,从而获得一个完整的结果。
    3.1 SQL解析

目前较为流行的sql解析器包括:并发

  • FoundationDB SQL Parser
  • Jsqlparser
  • Druid SQL Parser:解析性能最好,支持数据库方言最多

3.2 SQL路由
image.png框架

路由分则分为:

· 库规则:用于肯定到哪个分库

· 表规则:用于肯定到哪个分表

在上例中,咱们使用id来做为计算分表、分表,所以把id字段就称之为路由字段,或者分区字段。

须要注意的是,无论执行的是INSERT、UPDATE、DELETE、SELECT语句,SQL中都应该包含这个路由字段。不然,对于插入语句来讲,就不知道插入到哪一个分库或者分表;对于UPDATE、DELETE、SELECT语句而言,则更为严重,由于不知道操做哪一个分库分表,意味着必需要对全部分表都进行操做。SELECT聚合全部分表的内容,极容易内存溢出,UPDATE、DELETE更新、删除全部的记录,很是容易误更新、删除数据。所以,一些数据库中间件,对于SQL可能有一些限制,例如UPDATE、DELETE必需要带上分区字段,或者指定过滤条件。

路由引擎

https://shardingsphere.apache.org/document/current/cn/features/sharding/principle/route/

3.3 SQL 改写
前面已经介绍过,如一个批量插入语句,若是记录要插入到不一样的分库分表中,那么就须要对SQL进行改写。 例如,将如下SQL

insert into user(id,name) values (1,”tianshouzhi”),(2,”huhuamin”), (3,”wanghanao”),(4,”luyang”)

改写为

insert into user_1(id,name) values (1,”tianshouzhi”)insert into user_2(id,name) values (2,”huhuamin”)insert into user_3(id,name) values (3,”wanghanao”)insert into user_0(id,name) values (4,”luyang”)

这里只是一个简单的案例,一般对于INSERT、UPDATE、DELETE等,改写相对简单。比较复杂的是SELECT语句的改写,对于一些复杂的SELECT语句,改写过程当中会进行一些优化,例如将子查询改为JOIN,过滤条件下推等。由于SQL改写很复杂,因此不少数据库中间件并不支持复杂的SQL(一般有一个支持的SQL),只能支持一些简单的OLTP场景。

下表都是按照order_id 分表 改以前 改以后
标识符改写 SELECT order_id FROM t_order WHERE order_id=1; SELECT order_id FROM t_order_1 WHERE order_id=1;
排序补列 SELECT order_id FROM t_order ORDER BY user_id; SELECT order_id, user_id FROM t_order ORDER BY user_id;
聚合补列 SELECT AVG(price) FROM t_order WHERE user_id=1; SELECT COUNT(price) AS AVG_DERIVED_COUNT_0, SUM(price) AS AVG_DERIVED_ SUM _0 FROM t_order WHERE user_id=1;
自增主键补列 INSERT INTO t_order (field1, field2) VALUES (10, 1); INSERT INTO t_order (field1, field2, order_id) VALUES (10, 1, xxxxx);
批量插入查分 INSERT INTO t_order (order_id, xxx) VALUES (1, 'xxx'), (2, 'xxx'), (3, 'xxx'); INSERT INTO t_order_0 (order_id, xxx) VALUES (2, 'xxx'); INSERT INTO t_order_1 (order_id, xxx) VALUES (1, 'xxx'), (3, 'xxx');
in查询拆分 SELECT * FROM t_order WHERE order_id IN (1, 2, 3); vSELECT FROM t_order_0 WHERE order_id IN (2); SELECT FROM t_order_1 WHERE order_id IN (1, 3);

image.png

3.4 SQL 执行
当通过SQL改写阶段后,会产生多个SQL,须要到不一样的分片上去执行,一般咱们会使用一个线程池,将每一个SQL包装成一个任务,提交到线程池里面并发的去执行,以提高效率。

这些执行的SQL中,若是有一个失败,则总体失败,返回异常给业务代码。

3.5 结果集合并
结果集合并,是数据库中间件的一大难点,须要case by case的分析,主要是考虑实现的复杂度,以及执行的效率问题,对于一些复杂的SQL,可能并不支持。例如:

对于查询条件:大部分中间件都支持=、IN做为查询条件,且能够做为分区字段。可是对于NOT IN、BETWEEN…AND、LIKE,NOT LIKE等,只能做为普通的查询条件,由于根据这些条件,没法记录究竟是在哪一个分库或者分表,只能全表扫描。

聚合函数:大部分中间件都支持MAX、MIN、COUNT、SUM,可是对于AVG可能只是部分支持。另外,若是是函数嵌套、分组(GROUP BY)聚合,可能也有一些数据库中间件不支持。

 子查询:分为FROM部分的子查询和WHERE部分的子查询。大部分中对于子查询的支持都是很是有限,例如语法上兼容,可是没法识别子查询中的分区字段,或者要求子查询的表名必须与外部查询表名相同,又或者只能支持一级嵌套子查询。

 JOIN:对于JOIN的支持一般很复杂,若是作不到过滤条件下推和流式读取,在中间件层面,基本没法对JOIN进行支持,由于不可能把两个表的全部分表,所有拿到内存中来进行JOIN,内存早就崩了。固然也有一些取巧的办法,一个是Binding Table,另一个是小表广播(见后文)。

  分页排序:一般中间件都是支持ORDER BY和LIMIT的。可是在分库分表的状况下,分页的效率较低。例如对于limit 100,10 ORDER BY id。表示按照id排序,从第100个位置开始取10条记录。那么,大部分数据库中间件其实是要从每一个分表都查询110(100+10)条记录,拿到内存中进行从新排序,而后取出10条。假设有10个分表,那么实际上要查询1100条记录,而最终只过滤出了10记录。所以,在分页的状况下,一般建议使用"where id > ? limit 10”的方式来进行查询,应用记住每次查询的最大的记录id。以后查询时,每一个分表只须要从这个id以后,取10条记录便可,而不是取offset + rows条记录。 

关于JOIN的特属说明

  1. Binding Table: 强关联的表的路由规则设置为彻底同样,在同一个分库中join
  2. 小表广播: 每一个分库内都实时同步一份完整的数据,在同一个分库中join
相关文章
相关标签/搜索