当数据库中表的数据达到必定级别后,就须要考虑解决方案。事实上MySQL单表能够存储10亿级数据,只是这时候性能比较差,业界公认MySQL单表容量在1KW如下是最佳状态,由于这时它的BTREE索引树高在3~5之间。既然一张表没法搞定,那么就想办法将数据放到多个地方,目前比较广泛的方案可能有下列几种:java
归档适用场景:最新的数据会被常用,旧数据不多被使用。mysql
为何不用NoSQL/NewSQLsql
首先,为何不选择第三种方案NoSQL/NewSQL,RDBMS主要有如下几个优势:数据库
NoSQL/NewSQL做为新生儿,在咱们把可靠性当作首要考察对象时,它是没法与RDBMS相提并论的。RDBMS发展几十年,只要有软件的地方,它都是核心存储的首选。
目前绝大部分公司的核心数据都是:**以RDBMS存储为主,NoSQL/NewSQL存储为辅**!
目前互联网行业处理海量数据的通用方法:**分库分表**。
典型的数据库中间件设计方案有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 |
![]() |
![]() |
|
具体实现上有2种方案:hint 或APIapi
一些简单的选择策略包括:服务器
数据库中间件主要对应用屏蔽了如下过程:架构
目前较为流行的sql解析器包括:并发
3.2 SQL路由框架
路由分则分为:
· 库规则:用于肯定到哪个分库
· 表规则:用于肯定到哪个分表
在上例中,咱们使用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); |
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的特属说明: