一般,线上系统的业务量不是很大,比如说单库的数据量在百万级别以下,那么MySQL的单库即可完成任何增/删/改/查的业务操作。随着业务的发展,单个DB中保存的数据量(用户、订单、计费明细和权限规则等数据)呈现指数级增长,那么各种业务处理操作都会面临单DB的IO读写瓶颈带来的性能问题。MySQL的分区表比较适合用于解决业务数据具有较强时间序列特点,且数据量偏大的场景。但是,如果SQL的查询条件并非基于时间维度的,那么执行起来的效率并不会有所改观,同时对于处理单表千万级别以上数据量时,MySQL的分区表方案还是会显得有些“力不从心”。
使用垂直切分方案的主要优点如下:
a.拆分后业务清晰,符合微服务的总体设计理念;
b.子系统之间的整合与扩展相对容易;
c.按照不同的业务类型,将不同的库表放在不同的数据库服务器上,便于管理;
d.根据业务数据的“冷”、“热”状态,采用不同的缓存和数据库模式设计方案;
垂直切分的缺点如下:
a.跨库的Join查询,需要使用不同子系统的API接口读取后在内存中完成关联查询,提高系统复杂度;
b.如果某一种类型的业务呈现爆发式地增长,该业务对应的库表还是会存在单DB的IO读写的瓶颈问题,从这一点来说垂直切分并未从根本解决单库单表数据量过大的问题;
c.存在跨库事务的一致性问题,解决起来比较麻烦,当然也可以采用分布式事务来解决;
将其按照数据表中某个字段或某几个字段的某种规则切分存储至多个DB中,在每个库每个表中所包含的只是其中的一部分数据,所有库表加起来的才是全量的业务数据。
这种对数据的切分方式,基本可以保证经过水平切分后的单库单表存储的容量不会太大,从而保证了对单表的增/删/改/查的SQL执行效率和处理能力。其中,数据的分片规则也是需要重点考虑的,因为它会使得数据分片在多个库表中是否均匀分布存储。
使用水平切分方案的主要优点如下:
a.单库单表的数据容量可以维持在一个量级,有助于提高业务SQL的执行效率和系统性能;
b.不管是利用ShardingJdbc组件还是MyCat框架,业务系统的应用层涉及改造得都较少,只需要根据实际的业务情况来设计数据分片的路由规则即可;
c.可以提高业务系统的稳定性和负载能力;
使用水平切分方案的主要缺点如下:
a.数据水平切分后,分布在多库多表中,跨库Join查询比较复杂;
b.分片数据的一致性较为难保证;
c.对于历史数据的迁移和数据库的扩容需要较大的维护工作量;
分库分表用于应对当前互联网常见的两个场景——大数据量和高并发。通常分为垂直拆分和水平拆分两种。
垂直拆分是根据业务将一个库(表)拆分为多个库(表)。如:将经常和不常访问的字段拆分至不同的库或表中。由于与业务关系密切,目前的分库分表产品均使用水平拆分方式。
水平拆分则是根据分片算法将一个库(表)拆分为多个库(表)。如:按照ID的最后一位以3取余,尾数是1的放入第1个库(表),尾数是2的放入第2个库(表)等。
关系型数据库在大于一定数据量的情况下检索性能会急剧下降。在面对互联网海量数据情况时,所有数据都存于一张表,显然会轻易超过数据库表可承受的数据量阀值。这个单表可承受的数据量阀值,需根据数据库和并发量的差异,通过实际测试获得。
单纯的分表虽然可以解决数据量过大导致检索变慢的问题,但无法解决过多并发请求访问同一个库,导致数据库响应变慢的问题。所以通常水平拆分都至少要采用分库的方式,用于一并解决大数据量和高并发的问题。这也是部分开源的分片数据库中间件只支持分库的原因。但分表也有不可替代的适用场景。最常见的分表需求是事务问题。同在一个库则不需考虑分布式事务,善于使用同库不同表可有效避免分布式事务带来的麻烦。目前强一致性的分布式事务由于性能问题,导致使用起来并不一定比不分库分表快。目前采用最终一致性的柔性事务居多。分表的另一个存在的理由是,过多的数据库实例不利于运维管理。综上所述,最佳实践是合理地配合使用分库+分表。
客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的 Sharding-JDBC 、阿里的TDDL是两种比较常用的实现。
中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat、360的Atlas、网易的DDB等等都是这种架构的实现
Sharding-JDBC简介
Sharding-JDBC是当当应用框架ddframe中,从关系型数据库模块dd-rdb中分离出来的数据库水平分片框架,实现透明化数据库分库分表访问。Sharding-JDBC是继dubbox和elastic-job之后,ddframe系列开源的第3个项目。
Sharding-JDBC直接封装JDBC API,可以理解为增强版的JDBC驱动,旧代码迁移成本几乎为零:
可适用于任何基于Java的ORM框架,如JPA、Hibernate、Mybatis、Spring JDBC Template或直接使用JDBC。
可基于任何第三方的数据库连接池,如DBCP、C3P0、 BoneCP、Druid等。
理论上可支持任意实现JDBC规范的数据库。虽然目前仅支持MySQL,但已有支持Oracle、SQLServer等数据库的计划。
Sharding-JDBC定位为轻量Java框架,使用客户端直连数据库,以jar包形式提供服务,无proxy代理层,无需额外部署,无其他依赖,DBA也无需改变原有的运维方式。
Sharding-JDBC分片策略灵活,可支持等号、between、in等多维度分片,也可支持多分片键。
SQL解析功能完善,支持聚合、分组、排序、limit、or等查询,并支持Binding Table以及笛卡尔积表查询。
无论使用哪种架构,核心逻辑均极为相似,除了协议实现层不同(JDBC或数据库协议),都会分为分片规则配置、SQL解析、SQL改写、SQL路由、SQL执行以及结果归并等模块。
sharding-jdbc的版本及其架构差异
目前最新版本的sharding-jdbc是v3.0.0.M1,应该来说还不稳定,包名又改成了io.shardingsphere,jar包名是sharding-jdbc。
1.5.4.1是目前最新版,也可能是终版,1.x的,坐标是com.dangdang,jar包名是sharding-jdbc。
2.0.3是目前最新版,包名和坐标统一改成了io.shardingjdbc,jar包名是sharding-jdbc-core。
说明规划不是特别好,还是有些乱。
因为2.0之后基本上纯粹分库分表的核心特性的增强就不多了,主要往治理和代理方向去了,所以如果没有特别的需求比如需要类似mycat的独立服务代理模式,使用1.x(注:1.x版本官网文档好像下线了)就可以了,不过如果是大规模的部署,同时已经使用了微服务架构中的注册中心或者基于spring boot,可以考虑使用2.0,因为2.0增加了基于注册中心的配置管理以及spring boot starter。所以2.0的架构是这样的:
3.0之后,增加了类似mycat角色的sharding-proxy无状态服务器(代理层可以有,但是不应该作为应用直接访问的入口,如下图所示),以及类似于Service Mesh的Database Mesh。不过核心没有变化,对SQL语法增加了部分新的支持。所以3.0的架构如下:
就分库分表核心来说,我们就只要关心sharding-jdbc就可以了,不需要关心sharding-sphere的其他部分。
sharding jdbc 2.x不支持强一致性的分布式事务,一般现在的系统设计也不追求强一致性,而是最终一致性。所以sharding jdbc 2.x支持2中事务:弱XA(同mycat)和最大努力投递事务(官方简称BED)(算是BASE的一种),具体选择哪一种需要根据业务和开发进行权衡,如果架构规范和设计做得好,是可以做到不跨库分布式事务的。BED的架构如下
但是没有免费的午餐,BED对开发和维护有着一定的额外要求,而且这些要求都涉及面很广,绝对算得上伤筋动骨。开发层面包括:
- INSERT语句的主键不能自增
- UPDATE必须可重复执行,比如不支持UPDATE xxx SET x=x+1,对于更新余额类,这就相当于要求必须乐观锁了
运维层面包括:
- 需要存储事务日志的数据库
- 用于异步作业使用的zookeeper
- 解压sharding-jdbc-transaction-async-job-$VERSION.tar,通过start.sh脚本启动异步作业
我们选择了从设计层面避免强一致性的分布式事务。
假设我们不使用弱性事务(如果使用柔性事务,则还需要引入sharding-jdbc-transaction以及sharding-jdbc-transaction-async-job),这样就只要引入sharding-jdbc-core这个jar包就可以了,
<dependency>
<groupId>io.shardingjdbc</groupId>
<artifactId>sharding-jdbc-core</artifactId>
<version>2.0.3</version>
</dependency>
<!-- for sharding-jdbc spring namespace -->
<dependency>
<groupId>io.shardingjdbc</groupId>
<artifactId>sharding-jdbc-core-spring-namespace</artifactId>
<version>2.0.3</version>
</dependency>
Sharding-JDBC的整体架构图参见图1。
分片规则配置
Sharding-JDBC的分片逻辑非常灵活,支持分片策略自定义、复数分片键、多运算符分片等功能。
如:根据用户ID分库,根据订单ID分表这种分库分表结合的分片策略;或根据年分库,月份+用户区域ID分表这样的多片键分片。
Sharding-JDBC除了支持等号运算符进行分片,还支持in/between运算符分片,提供了更加强大的分片功能。
Sharding-JDBC提供了spring命名空间用于简化配置,以及规则引擎用于简化策略编写。
JDBC规范重写
Sharding-JDBC对JDBC规范的重写思路是针对DataSource、Connection、Statement、PreparedStatement和ResultSet五个核心接口封装,将多个真实JDBC实现类集合(如:MySQL JDBC实现/DBCP JDBC实现等)纳入Sharding-JDBC实现类管理。
Sharding-JDBC尽量最大化实现JDBC协议,包括addBatch这种在JPA中会使用的批量更新功能。但分片JDBC毕竟与原生JDBC不同,所以目前仍有未实现的接口,包括Connection游标,存储过程和savePoint相关、ResultSet向前遍历和修改等不太常用的功能。此外,为了保证兼容性,并未实现JDBC 4.1及其后发布的接口(如:DBCP 1.x版本不支持JDBC 4.1)。
SQL解析
SQL解析作为分库分表类产品的核心,性能和兼容性是最重要的衡量指标。目前常见的SQL解析器主要有fdb/jsqlparser和Druid。Sharding-JDBC使用Druid作为SQL解析器,经实际测试,Druid解析速度是另外两个解析器的几十倍。
目前Sharding-JDBC支持join、aggregation(包括avg)、order by、 group by、limit、甚至or查询等复杂SQL的解析。目前不支持union、部分子查询、函数内分片等不太应在分片场景中出现的SQL解析。
SQL改写
SQL改写分为两部分,一部分是将分表的逻辑表名称替换为真实表名称。另一部分是根据SQL解析结果替换一些在分片环境中不正确的功能。这里具两个例子:
第1个例子是avg计算。在分片的环境中,以avg1 +avg2+avg3/3计算平均值并不正确,需要改写为(sum1+sum2+sum3)/(count1+count2+ count3)。这就需要将包含avg的SQL改写为sum和count,然后再结果归并时重新计算平均值。
第2个例子是分页。假设每10条数据为一页,取第2页数据。在分片环境下获取limit 10, 10,归并之后再根据排序条件取出前10条数据是不正确的结果。正确的做法是将分条件改写为limit 0, 20,取出所有前2页数据,再结合排序条件算出正确的数据。可以看到越是靠后的Limit分页效率就会越低,也越浪费内存。有很多方法可避免使用limit进行分页,比如构建记录行记录数和行偏移量的二级索引,或使用上次分页数据结尾ID作为下次查询条件的分页方式。
SQL路由
SQL路由是根据分片规则配置,将SQL定位至真正的数据源。主要分为单表路由、Binding表路由和笛卡尔积路由。
单表路由最为简单,但路由结果不一定落入唯一库(表),因为支持根据between和in这样的操作符进行分片,所以最终结果仍然可能落入多个库(表)。
Binding表可理解为分库分表规则完全一致的主从表。举例说明:订单表和订单详情表都根据订单ID作为分片键,任意时刻分片逻辑均相同。这样的关联查询和单表查询难度和性能相当。
笛卡尔积查询最为复杂,因为无法根据Binding关系定位分片规则的一致性,所以非Binding表的关联查询需要拆解为笛卡尔积组合执行。查询性能较低,而且数据库连接数较高,需谨慎使用。
SQL执行
路由至真实数据源后,Sharding-JDBC将采用多线程并发执行SQL,并完成对addBatch等批量方法的处理。
结果归并
结果归并包括4类:普通遍历类、排序类、聚合类和分组类。每种类型都会先根据分页结果跳过不需要的数据。
普通遍历类最为简单,只需按顺序遍历ResultSet的集合即可。
排序类结果将结果先排序再输出,因为各分片结果均按照各自条件完成排序,所以采用归并排序算法整合最终结果。
聚合类分为3种类型,比较型、累加型和平均值型。比较型包括max和min,只返回最大(小)结果。累加型包括sum和count,需要将结果累加后返回。平均值则是通过SQL改写的sum和count计算,相关内容已在SQL改写涵盖,不再赘述。
分组类最为复杂,需要将所有的ResultSet结果放入内存,使用map-reduce算法分组,最后根据排序和聚合条件做相关处理。最消耗内存,最损失性能的部分即是此,可以考虑使用limit合理的限制分组数据大小。
结果归并部分目前并未采用管道解析的方式,之后会针对这里做更多改进。