Mysql慢查询优化小记

1.背景

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

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

归档适用场景:最新的数据会被经常使用,旧数据很少被使用。

为什么不用NoSQL/NewSQL

首先,为什么不选择第三种方案NoSQL/NewSQL,RDBMS主要有以下几个优点:

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

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

Mysql慢查询优化小记_第1张图片

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

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

3读写分离核心要点

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

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

  • 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路由
Mysql慢查询优化小记_第3张图片

路由分则分为:

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

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

在上例中,我们使用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);

Mysql慢查询优化小记_第4张图片

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

你可能感兴趣的:(mysql)