总结使用shardingJDBC分表事项

总结使用shardingJDBC分表事项

一、使用场景:

使用场景就很简单了,减小单表crud压力。平时在做电商项目,针对下单这一步肯定是要求后台业务对数据库层对crud效率更快,并且在系统以后的推广中数据量越来越大这种情况能有更好的解决措施,那么这一点就可以使用sharding-jdbc分库分表

但这次只总结分表的使用方法。直接上代码。

具体代码

环境:spring-boot 2.0 、jdk 1.8  、mysql(具体版本忘记了,一般高版本也没啥问题)

第一步:分表规则(根据区域编码进行分组,以表order为例)

在数据库中创建表的order_01,order_02...必须提前创建好,不然无法启动,每张表都需要分区键partition_key用于存储规则,比如: _01,_02...;

第二步:自定义表 sharding_mapping_table,这个业务表主要是根据请求参数带的区域编码(比如:01、02)来获取表的后缀(_01,_02)或者全表名(order_01,order_02),这里是获取的后缀;

第三步:application.properties配置

#你的库名
sharding.jdbc.datasource.names = order_data_base
##下面三个是一些常规的配置
sharding.jdbc.datasource.order_data_base.encode = false
sharding.jdbc.datasource.order_data_base.type = com.alibaba.druid.spring.boot.autoconfigure.DruidWrapper
sharding.jdbc.datasource.order_data_base.driver-class-name = com.mysql.cj.jdbc.Driver
##数据库配置
sharding.jdbc.datasource.order_data_base.url = jdbc:mysql://******
sharding.jdbc.datasource.order_data_base.username = ****
sharding.jdbc.datasource.order_data_base.password = ****
##这里是具体的分表算法,提前先把分表的所有表结构填写好,默认查询主表
sharding.jdbc.config.sharding.tables.order.actual-data-nodes = order_data_base.order$->{["_01","_02"...]}
sharding.jdbc.config.sharding.tables.md_character.actual-data-nodes = order_data_base.order_detail$->{["_01","_02"...]}
##分区键
sharding.jdbc.config.sharding.default-table-strategy.standard.sharding-column = partition_key
##指定策略,这里就是要定位到具体的算法
sharding.jdbc.config.sharding.default-table-strategy.standard.precise-algorithm-class-name = com.order.service.shardingconfig.RegionCodeShardingConfig
sharding.jdbc.config.sharding.default-table-strategy.standard.range-algorithm-class-name = com.order.service.shardingconfig.RegionCodeShardingConfig

第四步:算法com.order.service.shardingconfig.RegionCodeShardingConfig

@Slf4j
@Component
public class RegionCodeShardingConfig implements PreciseShardingAlgorithm, RangeShardingAlgorithm {


@Override
public String doSharding(Collection availableTargetNames, PreciseShardingValue shardingValue) {
String tableName = shardingValue.getLogicTableName();
MdShardingMapping shardingMapping = ShardingAspect.get();
//如果获取前端传入区域编码无效,那么就默认查询原表
if (shardingMapping != null) {
//这里就是具体 order+_01拼接表 order_01
String str = tableName + shardingMapping.getTableNameSubfix().toLowerCase();
if (availableTargetNames.contains(str)) {
tableName = str;
}
}
return tableName;
}

@Override
public Collection<String> doSharding(Collection availableTargetNames, RangeShardingValue shardingValue) {
return availableTargetNames;
}

}

 

第五步:切面解析区域编码

@Slf4j
@Aspect
@Component
public class ShardingAspect {

private static ThreadLocal<MdShardingMapping> shardingMappingThreadLocal = new ThreadLocal<>();
/**
* 订单表的唯一表识
*/
private final static String KEY_PREFIX_ONE="10";

/**
* MdShardingMapping这张表是具体根据你的实际分表情况查询表的后缀规则,比如根据order就查询到_01,_02
*/
@Autowired
private MdShardingMappingRepository mdShardingMappingRepository;

/**
* 拦截order的请求
*/
@Pointcut("execution(public * com.order.service.impl.OrderServiceImpl.*(..))" +
"|| execution(public * com.order.service.impl.OrderServiceImpl.*(..)) ")
public void addShardingOrder() {
}

@Before("addShardingOrder()")
public void doBeforeOrder(JoinPoint joinPoint) throws Throwable {
//这个是解析的前端单点登录的登录人信息,里面包含区域编码
PublicParam param = SessionUtil.get();
if (shardingMappingThreadLocal.get() != null || param == null) {
return;
}
String shardingKeyValue =param.getRegionCode();
//根据唯一条件查询分区规则
MdShardingMapping shardingMapping = mdShardingMappingRepository
.findByApplicationSystemIdAndShardingKeyValueAndPrimaryKeyPrefix(param.getApplicationId(), shardingKeyValue,KEY_PREFIX_ONE);
//存入本地线程
shardingMappingThreadLocal.set(shardingMapping);
}

@After("addShardingOrder()")
public void doAfterOrder() throws Throwable {
// 处理完请求,返回内容
shardingMappingThreadLocal.remove();
}
}

二、sharding-jdbc分区算法

2.1 简介:

Sharding-JDBC是当当开源的数据库水平切分的中间件,其代表了客户端类的分库分表技术框架(这一点与MyCat不同,MyCat本质上是一种数据库代理)。Sharding-JDBC定位为轻量级数据库驱动,由客户端直连数据库,以jar包形式提供服务,未使用中间层,无需额外部署,无其他依赖,业务系统开发人员与数据库运维人员无需改变原有的开发与运维方式。因此Sharding-JDBC即为增强版的JDBC驱动,可以实现旧代码迁移零成本的目标。 目前社区较为活跃。目前已广泛应用于现各大互联网公司。通过社区得知,Sharding-JDBC的作者已去京东做全职的Sharding-JDBC开发,相信未来Sharding-JDBC社区的发展将会更好。

2.2 功能介绍

2.2.1 Sharding-JDBC 

定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。

  适用于任何基于Java的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
  基于任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
  支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer和PostgreSQL。

 

2.2.2 Sharding-Proxy

定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本,用于完成对异构语言的支持。 目前先提供MySQL版本,它可以使用任何兼容MySQL协议的访问客户端(如:MySQL Command Client, MySQL Workbench等)操作数据,对DBA更加友好。

  向应用程序完全透明,可直接当做MySQL使用。
  适用于任何兼容MySQL协议的客户端。

 

2.2.3 Sharding-Sidecar

定位为Kubernetes或Mesos的云原生数据库代理,以DaemonSet的形式代理所有对数据库的访问。 通过无中心、零侵入的方案提供与数据库交互的的啮合层,即Database Mesh,又可称数据网格。

Database Mesh的关注重点在于如何将分布式的数据访问应用与数据库有机串联起来,它更加关注的是交互,是将杂乱无章的应用与数据库之间的交互有效的梳理。使用Database Mesh,访问数据库的应用和数据库终将形成一个巨大的网格体系,应用和数据库只需在网格体系中对号入座即可,它们都是被啮合层所治理的对象

具体算法:

逻辑表
水平拆分的数据表的总称。例:订单数据表根据主键尾数拆分为10张表,分别是 t_order_0 、 t_order_1 到t_order_9 ,他们的逻辑表名为 t_order 
真实表
在分片的数据库中真实存在的物理表。即上个示例中的 t_order_0 到 t_order_9 

数据节点
数据分片的最小物理单元。由数据源名称和数据表组成,例: ds_0.t_order_0 
绑定表
指分片规则一致的主表和子表。例如: t_order 表和 t_order_item 表,均按照 order_id 分片,绑定表之间的分区
键完全相同,则此两张表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大
大提升。举例说明,如果SQL为:

SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

 

在不配置绑定表关系时,假设分片键 order_id 将数值10路由至第0片,将数值11路由至第1片,那么路由后的SQL
应该为4条,它们呈现为笛卡尔积: 

SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

在配置绑定表关系后,路由的SQL应该为2条:

SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11); SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

广播表
指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致。适用于数据量不大且需要与
海量数据的表进行关联查询的场景,例如:字典表。
分片键
用于分片的数据库字段,是将数据库()水平拆分的关键字段。例:将订单表中的订单主键的尾数取模分片,则订
单主键为分片字段。 SQL中如果无分片字段,将执行全路由,性能较差。 除了对单分片字段的支持,ShardingJdbc也支持根据多个字段进行分片。
分片算法
通过分片算法将数据分片,支持通过 、 BETWEEN 和 IN 分片。分片算法需要应用方开发者自行实现,可实现的灵
活度非常高。包括:精确分片算法 、范围分片算法 ,复合分片算法 等。例如:where order_id = ? 将采用精确分
片算法,where order_id in (?,?,?)将采用精确分片算法,where order_id BETWEEN ? and ? 将采用范围分片算
法,复合分片算法用于分片键有多个复杂情况。
分片策略
包含分片键和分片算法,由于分片算法的独立性,将其独立抽离。真正可用于分片操作的是分片键 分片算法,也
就是分片策略。内置的分片策略大致可分为尾数取模、哈希、范围、标签、时间等。由用户方配置的分片策略则更
加灵活,常用的使用行表达式配置分片策略,它采用Groovy表达式表示,如t_user_$->{u_id % 8} 表示t_user
表根据u_id8,而分成8张表,表名称为 t_user_0 到 t_user_7 
自增主键生成策略
通过在客户端生成自增主键替换以数据库原生自增主键的方式,做到分布式主键无重复。

.SQL解析
Sharding-JDBC接受到一条SQL语句时,会陆续执行 SQL解析 => 查询优化 => SQL路由 => SQL改写 => SQL执行 =>
结果归并 ,最终返回执行结果。

SQL解析过程分为词法解析和语法解析。 词法解析器用于将SQL拆解为不可再分的原子符号,称为Token。并根据
不同数据库方言所提供的字典,将其归类为关键字,表达式,字面量和操作符。 再使用语法解析器将SQL转换为抽
象语法树。
例如,以下SQL:

SELECT id, name FROM t_user WHERE status = 'ACTIVE' AND age > 18

解析之后的为抽象语法树见下图:

 

 

 

 

为了便于理解,抽象语法树中的关键字的Token用绿色表示,变量的Token用红色表示,灰色表示需要进一步拆分。
最后,通过对抽象语法树的遍历去提炼分片所需的上下文,并标记有可能需要SQL改写(后边介绍)的位置。 供分片
使用的解析上下文包含查询选择项(Select Items)、表信息(Table)、分片条件(Sharding Condition)、自增
主键信息(Auto increment Primary Key)、排序信息(Order By)、分组信息(Group By)以及分页信息
(Limit、Rownum、Top)。
SQL路由
SQL路由就是把针对逻辑表的数据操作映射到对数据结点操作的过程。
根据解析上下文匹配数据库和表的分片策略,并生成路由路径。 对于携带分片键的SQL,根据分片键操作符不同可
以划分为单片路由(分片键的操作符是等号)、多片路由(分片键的操作符是IN)和范围路由(分片键的操作符是
BETWEEN),不携带分片键的SQL则采用广播路由。根据分片键进行路由的场景可分为直接路由、标准路由、笛卡尔路由等。

标准路由
标准路由是Sharding-Jdbc最为推荐使用的分片方式,它的适用范围是不包含关联查询或仅包含绑定表之间关联查
询的SQL。 当分片运算符是等于号时,路由结果将落入单库(表),当分片运算符是BETWEEN或IN时,则路由结
果不一定落入唯一的库(表),因此一条逻辑SQL最终可能被拆分为多条用于执行的真实SQL。 举例说明,如果按
照 order_id 的奇数和偶数进行数据分片,一个单表查询的SQL如下:

SELECT * FROM t_order WHERE order_id IN (1, 2);

那么路由的结果应为:

SELECT * FROM t_order_0 WHERE order_id IN (1, 2); SELECT * FROM t_order_1 WHERE order_id IN (1, 2);

 

绑定表的关联查询与单表查询复杂度和性能相当。举例说明,如果一个包含绑定表的关联查询的 SQL如下:

SELECT * FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id  WHERE order_id IN (1, 2);

那么路由的结果应为:

SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id  WHERE order_id IN (1, 2); SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id  WHERE order_id IN (1, 2);

可以看到,SQL拆分的数目与单表是一致的。
笛卡尔路由
笛卡尔路由是最复杂的情况,它无法根据绑定表的关系定位分片规则,因此非绑定表之间的关联查询需要拆解为笛
卡尔积组合执行。 如果上个示例中的SQL并未配置绑定表关系,那么路由的结果应为:

SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id  WHERE order_id IN (1, 2); SELECT * FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id  WHERE order_id IN (1, 2); SELECT * FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id  WHERE order_id IN (1, 2); SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id  WHERE order_id IN (1, 2);

笛卡尔路由查询性能较低,需谨慎使用。
全库表路由
对于不携带分片键的SQL,则采取广播路由的方式。根据SQL类型又可以划分为全库表路由、全库路由、全实例路
由、单播路由和阻断路由这5种类型。其中全库表路由用于处理对数据库中与其逻辑表相关的所有真实表的操作,
主要包括不带分片键的DQL(数据查询)和DML(数据操纵),以及DDL(数据定义)等。例如:

SELECT * FROM t_order WHERE good_prority IN (1, 10);

则会遍历所有数据库中的所有表,逐一匹配逻辑表和真实表名,能够匹配得上则执行。路由后成为

SELECT * FROM t_order_0 WHERE good_prority IN (1, 10); SELECT * FROM t_order_1 WHERE good_prority IN (1, 10); SELECT * FROM t_order_2 WHERE good_prority IN (1, 10); SELECT * FROM t_order_3 WHERE good_prority IN (1, 10);

SQL改写
工程师面向逻辑表书写的SQL,并不能够直接在真实的数据库中执行,SQL改写用于将逻辑SQL改写为在真实数据
库中可以正确执行的SQL。
如一个简单的例子,若逻辑SQL为:

SELECT order_id FROM t_order WHERE order_id=1;

假设该SQL配置分片键order_id,并且order_id=1的情况,将路由至分片表1。那么改写之后的SQL应该为:

SELECT order_id FROM t_order_1 WHERE order_id=1;

再比如,Sharding-JDBC需要在结果归并时获取相应数据,但该数据并未能通过查询的SQL返回。 这种情况主要是
针对GROUP BY和ORDER BY。结果归并时,需要根据 GROUP BY 和 ORDER BY 的字段项进行分组和排序,但如果原
始SQL的选择项中若并未包含分组项或排序项,则需要对原始SQL进行改写。 先看一下原始SQL中带有结果归并所
需信息的场景:

SELECT order_id, user_id FROM t_order ORDER BY user_id;

由于使用user_id进行排序,在结果归并中需要能够获取到user_id的数据,而上面的SQL是能够获取到user_id数据
的,因此无需补列。
如果选择项中不包含结果归并时所需的列,则需要进行补列,如以下SQL:

SELECT order_id FROM t_order ORDER BY user_id;

由于原始SQL中并不包含需要在结果归并中需要获取的user_id,因此需要对SQL进行补列改写。补列之后的SQL
是:

SELECT order_id, user_id AS ORDER_BY_DERIVED_0 FROM t_order ORDER BY user_id;

SQL执行
Sharding-JDBC采用一套自动化的执行引擎,负责将路由和改写完成之后的真实SQL安全且高效发送到底层数据源
执行。 它不是简单地将SQL通过JDBC直接发送至数据源执行;也并非直接将执行请求放入线程池去并发执行。它
更关注平衡数据源连接创建以及内存占用所产生的消耗,以及最大限度地合理利用并发等问题。 执行引擎的目标是
自动化的平衡资源控制与执行效率,他能在以下两种模式自适应切换:
内存限制模式

使用此模式的前提是, Sharding-JDBC对一次操作所耗费的数据库连接数量不做限制。 如果实际执行的SQL需要对
某数据库实例中的200张表做操作,则对每张表创建一个新的数据库连接,并通过多线程的方式并发处理,以达成
执行效率最大化。
连接限制模式
使用此模式的前提是,Sharding-JDBC严格控制对一次操作所耗费的数据库连接数量。 如果实际执行的SQL需要对
某数据库实例中的200张表做操作,那么只会创建唯一的数据库连接,并对其200张表串行处理。 如果一次操作中
的分片散落在不同的数据库,仍然采用多线程处理对不同库的操作,但每个库的每次操作仍然只创建一个唯一的数
据库连接。
内存限制模式适用于OLAP操作,可以通过放宽对数据库连接的限制提升系统吞吐量; 连接限制模式适用于OLTP操
作,OLTP通常带有分片键,会路由到单一的分片,因此严格控制数据库连接,以保证在线系统数据库资源能够被
更多的应用所使用,是明智的选择。

.结果归并
将从各个数据节点获取的多数据结果集,组合成为一个结果集并正确的返回至请求客户端,称为结果归并。
Sharding-JDBC支持的结果归并从功能上可分为遍历、排序、分组、分页和聚合5种类型,它们是组合而非互斥的
关系。
归并引擎的整体结构划分如下图。

结果归并从结构划分可分为流式归并、内存归并和装饰者归并。流式归并和内存归并是互斥的,装饰者归并可以在
流式归并和内存归并之上做进一步的处理。
内存归并很容易理解,他是将所有分片结果集的数据都遍历并存储在内存中,再通过统一的分组、排序以及聚合等
计算之后,再将其封装成为逐条访问的数据结果集返回

流式归并 是指每一次从数据库结果集中获取到的数据,都能够通过游标逐条获取的方式返回正确的单条数据,它与
数据库原生的返回结果集的方式最为契合。
下边举例说明排序归并的过程,如下图是一个通过分数进行排序的示例图,它采用流式归并方式。 图中展示了3张
表返回的数据结果集,每个数据结果集已经根据分数排序完毕,但是3个数据结果集之间是无序的。 将3个数据结
果集的当前游标指向的数据值进行排序,并放入优先级队列,t_score_0的第一个数据值最大,t_score_2的第一个
数据值次之,t_score_1的第一个数据值最小,因此优先级队列根据t_score_0,t_score_2和t_score_1的方式排序
队列。

 

 

 

下图则展现了进行next调用的时候,排序归并是如何进行的。 通过图中我们可以看到,当进行第一次next调用
时,排在队列首位的t_score_0将会被弹出队列,并且将当前游标指向的数据值(也就是100)返回至查询客户端,
并且将游标下移一位之后,重新放入优先级队列。 而优先级队列也会根据t_score_0的当前数据结果集指向游标的
数据值(这里是90)进行排序,根据当前数值,t_score_0排列在队列的最后一位。 之前队列中排名第二的
t_score_2的数据结果集则自动排在了队列首位。
在进行第二次next时,只需要将目前排列在队列首位的t_score_2弹出队列,并且将其数据结果集游标指向的值返
回至客户端,并下移游标,继续加入队列排队,以此类推。 当一个结果集中已经没有数据了,则无需再次加入队
列。

可以看到,对于每个数据结果集中的数据有序,而多数据结果集整体无序的情况下,Sharding-JDBC无需将所有的
数据都加载至内存即可排序。 它使用的是流式归并的方式,每次next仅获取唯一正确的一条数据,极大的节省了
内存的消耗。
装饰者归并是对所有的结果集归并进行统一的功能增强,比如归并时需要聚合SUM前,在进行聚合计算前,都会通
过内存归并或流式归并查询出结果集。因此,聚合归并是在之前介绍的归并类型之上追加的归并能力,即装饰者模
式。

总结
通过以上内容介绍,相信大家已经了解到Sharding-JDBC基础概念、核心功能以及执行原理。
基础概念:逻辑表,真实表,数据节点,绑定表,广播表,分片键,分片算法,分片策略,主键生成策略
核心功能:数据分片,读写分离
执行流程: SQL 解析 => 查询优化 => SQL路由 => SQL改写 => SQL执行 => 结果归并

posted @ 2020-11-12 10:40  心思慕晨  阅读(677)  评论(0)    收藏  举报