一、ShardingSphere-JDBC是什么
Apache ShardingSphere是一套开源的分布式数据库中间件解决方案组成的生态圈,它由 JDBC、Proxy 和 Sidecar(规划中)这 3 款相互独立却又能够混合部署配合使用的产品组成。
定位为轻量级 Java 框架,在Java的JDBC层提供的额外服务。它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。
适用于任何基于JDBC的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
支持任何第三方的数据库连接池,如:DBCP,C3P0,BoneCP,Druid,HikariCP等。
支持任意实现JDBC规范的数据库。目前支持 MySQL,Oracle,SQLServer,PostgreSQL以及任何遵循SQL92标准的数据库。
二、ShardingSphere-JDBC的核心功能
数据分片
1.分库分表
2.读写分离
3.分片策略可定制
4.无中心化分布式主键(snowflake算法)
分布式事务
1.标准化事务接口
2.两阶段提交事务
3.柔性事务
数据库治理
1.配置动态化
2.编排和治理
3.数据脱敏
4.可视化链路追踪
三、概念介绍
Sharding JDBC的整个执行过程涉及到一些概念:
1、逻辑表
水平拆分的数据表的总称。例如:我们将用户表根据某种策略拆分为n张表,分别是 t_user_0 、 t_user_1 、…、t_user_n,他们的逻辑表名为 t_order。
2、真实表
在分片的数据库中真实存在的物理表。即上述示例中的t_user_0 、 t_user_1 、…、t_user_n。
3、数据节点
数据分片的最小物理单元。由数据源名称和数据表组成,例如: DS_0.t_user_0、DS_0.t_user_1、DS_1.t_user_0、DS_1.t_user_1。
4、绑定表-联表查询防止出现笛卡尔积现象
指分片规则一致的主表和子表。例如: 有t_user和 t_user_detail两个逻辑表,两个表都按照user_id分片,绑定表之间的分区
键完全相同,则此两张表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大
大提升。举例说明,如果逻辑SQL为:
SELECT d.* FROM t_user u JOIN t_user_detail d ON u.user_id=d.user_id WHERE u.user_id in (1,2);
在不配置绑定表关系时,假设分片键 user_id 将数值1路由至第0片,将数值2路由至第1片,那么路由后的SQL
应该为4条,它们呈现为笛卡尔积:
SELECT d.* FROM t_user_0 u JOIN t_user_detail_0 d ON u.user_id=d.user_id WHERE u.user_id in (1,2);
SELECT d.* FROM t_user_0 u JOIN t_user_detail_1 d ON u.user_id=d.user_id WHERE u.user_id in (1,2);
SELECT d.* FROM t_user_1 u JOIN t_user_detail_0 d ON u.user_id=d.user_id WHERE u.user_id in (1,2);
SELECT d.* FROM t_user_1 u JOIN t_user_detail_1 d ON u.user_id=d.user_id WHERE u.user_id in (1,2);
在配置绑定表关系后,路由的SQL应该为2条:
SELECT d.* FROM t_user_0 u JOIN t_user_detail_0 d ON u.user_id=d.user_id WHERE u.user_id in (1,2);
SELECT d.* FROM t_user_1 u JOIN t_user_detail_1 d ON u.user_id=d.user_id WHERE u.user_id in (1,2);
5、广播表
也可以叫公共表,指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致。适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。
6、分片键
用于分片的数据库字段,是将数据库(表)水平拆分的关键字段。例:将用户表中的用户ID取模分片,则用户ID为分片字段。SQL中如果无分片字段,将执行全路由,性能较差。除了对单分片字段的支持,ShardingJDBC也支持根据多个字段进行分片。
7、分片算法
通过分片算法将数据分片,支持通过 = 、BETWEEN和IN分片。分片算法需要应用方开发者自行实现,可实现的灵活度非常高。包括:精确分片算法 、范围分片算法 ,复合分片算法 等。例如:where user_id = ? 将采用精确分片算法,where user_id in (?,?,?)将采用精确分片算法,where user_id BETWEEN ? and ? 将采用范围分片算法,复合分片算法用于分片键有多个复杂情况。
8、分片策略
包含分片键和分片算法,由于分片算法的独立性,将其独立抽离。真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。内置的分片策略大致可分为取模、哈希、范围、标签、时间等。由用户方配置的分片策略则更加灵活,常用的使用行表达式配置分片策略,它采用Groovy表达式表示,如: t_user_$->{user_id % 50} 表示t_user表根据user_id模50,而分成50张表,表名称为 t_user_0 到 t_user_49。
9、自增主键生成策略
通过在客户端生成自增主键替换以数据库原生自增主键的方式,做到分布式主键无重复。
四、ShardingSphere-JDBC执行原理
上图所示,描述了Sharding JDBC的大致执行原理图。
1、SQL解析
当Sharding-JDBC接受到一条SQL语句时,会陆续执行SQL解析 -> 查询优化 -> SQL路由 -> SQL改写 -> SQL执行 ->结果归并 ,最终返回执行结果。
SQL解析过程包括词法解析和语法解析。词法解析器用于将SQL拆解为不可再分的原子符号,称为Token。并根据不同数据库方言所提供的字典,将其归类为关键字、表达式、字面量和操作符。再使用语法解析器将SQL转换为抽象语法树。
比如,有以下查询SQL:
SELECT user_id,name FROM t_user WHERE status = 'NORMAL' AND age > 18;
解析之后的为抽象语法树见下图:
通过对抽象语法树的遍历去提炼分片所需的上下文,并标记有可能需要SQL改写的位置。 供分片使用的解析上下文包含查询选择项(Select Items)、表信息(Table)、分片条件(Sharding Condition)、自增主键信息(Auto increment Primary Key)、排序信息(Order By)、分组信息(Group By)以及分页信息(Limit、Rownum、Top)。
2、SQL路由
SQL路由就是把针对逻辑表的数据操作映射到对数据结点操作的过程。根据解析上下文匹配数据库和表的分片策略,并生成路由路径。对于携带分片键的SQL,根据分片键操作符不同可以划分为单片路由(分片键的操作符是等号)、多片路由(分片键的操作符是IN)和范围路由(分片键的操作符是BETWEEN),不携带分片键的SQL则采用广播路由。根据分片键进行路由的场景可分为直接路由、标准路由、笛卡尔路由等。
标准路由
标准路由是Sharding-Jdbc最为推荐使用的分片方式,它的适用范围是不包含关联查询或仅包含绑定表之间关联查询的SQL。 当分片运算符是等于号时,路由结果将落入单库(表),当分片运算符是BETWEEN或IN时,则路由结果不一定落入唯一的库(表),因此一条逻辑SQL最终可能被拆分为多条用于执行的真实SQL。
笛卡尔路由
笛卡尔路由是最复杂的情况,它无法根据绑定表的关系定位分片规则,因此非绑定表之间的关联查询需要拆解为笛卡尔积组合执行。
全库表路由
对于不携带分片键的SQL,则采取广播路由的方式。根据SQL类型又可以划分为全库表路由、全库路由、全实例路由、单播路由和阻断路由这5种类型。其中全库表路由用于处理对数据库中与其逻辑表相关的所有真实表的操作,主要包括不带分片键的DQL(数据查询)和DML(数据操纵),以及DDL(数据定义)等。
3、SQL改写
我们在开发过程中面向逻辑表书写的SQL并不能够直接在真实的数据库中执行,需要将逻辑SQL改写为能在真实数据库中可以正确执行的SQL。
举一个简单的例子,若逻辑SQL为:
SELECT name FROM t_user WHERE user_id=1;
假设该SQL配置分片键user_id,并且user_id=1的情况将路由至分片表1。那么改写之后的SQL应该为:
SELECT name FROM t_user_1 WHERE user_id=1;
还有一种情况,当Sharding-JDBC需要在结果归并时获取相应数据,但该数据并未能通过查询的SQL返回。 这种情况主要是针对GROUP BY和ORDER BY。结果归并时,需要根据GROUP BY和ORDER BY的字段项进行分组和排序,但如果原始SQL的选择项中若并未包含分组项或排序项,则需要对原始SQL进行改写。比如有这样一个逻辑SQL:
SELECT name FROM t_user ORDER BY create_time;
由于原始SQL中并不包含需要在结果归并中需要获取的,因此需要对SQL进行补列改写。补列之后的SQL是:
SELECT name, create_time FROM t_user ORDER BY create_time;
4、SQL执行
Sharding-JDBC采用一套自动化的执行引擎,负责将路由和改写完成之后的真实SQL安全且高效发送到底层数据源执行。 它不是简单地将SQL通过JDBC直接发送至数据源执行,也不是直接将执行请求放入线程池去并发执行。它更关注平衡数据源连接创建以及内存占用所产生的消耗,以及最大限度地合理利用并发等问题。 执行引擎的目标是自动化的平衡资源控制与执行效率,他能在以下两种模式自适应切换:
内存限制模式
使用此模式的前提是, Sharding-JDBC对一次操作所耗费的数据库连接数量不做限制。 如果实际执行的SQL需要对某数据库实例中的200张表做操作,则对每张表创建一个新的数据库连接,并通过多线程的方式并发处理,以达成执行效率最大化。
连接限制模式
使用此模式的前提是,Sharding-JDBC严格控制对一次操作所耗费的数据库连接数量。 如果实际执行的SQL需要对某数据库实例中的200张表做操作,那么只会创建唯一的数据库连接,并对其200张表串行处理。 如果一次操作中的分片散落在不同的数据库,仍然采用多线程处理对不同库的操作,但每个库的每次操作仍然只创建一个唯一的数据库连接。
内存限制模式适用于OLAP操作,可以通过放宽对数据库连接的限制提升系统吞吐量; 连接限制模式适用于OLTP操作,OLTP通常带有分片键,会路由到单一的分片,因此严格控制数据库连接,以保证在线系统数据库资源能够被更多的应用所使用,是明智的选择。
5、结果归并
将从各个数据节点获取的多数据结果集,组合成为一个结果集并正确的返回至请求客户端,称为结果归并。
结果归并从功能划分可分为:遍历、排序、分组、分页和聚合5种类型,它们是组合而非互斥的关系。
遍历归并
它是最为简单的归并方式。 只需将多个数据结果集合并为一个单向链表即可。在遍历完成链表中当前数据结果集之后,将链表元素后移一位,继续遍历下一个数据结果集即可。
排序归并
由于在SQL中存在ORDER BY语句,因此每个数据结果集自身是有序的,因此只需要将数据结果集当前游标指向的数据值进行排序即可。 这相当于对多个有序的数组进行排序,归并排序是最适合此场景的排序算法。
ShardingSphere在对排序的查询进行归并时,将每个结果集的当前数据值进行比较(通过实现Java的Comparable接口完成),并将其放入优先级队列。 每次获取下一条数据时,只需将队列顶端结果集的游标下移,并根据新游标重新进入优先级排序队列找到自己的位置即可。
通过一个例子来说明ShardingSphere的排序归并,下图是一个通过分数进行排序的示例图。 图中展示了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弹出队列,并且将其数据结果集游标指向的值返回至客户端,并下移游标,继续加入队列排队,以此类推。 当一个结果集中已经没有数据了,则无需再次加入队列。
可以看到,对于每个数据结果集中的数据有序,而多数据结果集整体无序的情况下,ShardingSphere无需将所有的数据都加载至内存即可排序。 它使用的是流式归并的方式,每次next仅获取唯一正确的一条数据,极大的节省了内存的消耗。
聚合归并
无论是流式分组归并还是内存分组归并,对聚合函数的处理都是一致的。 除了分组的SQL之外,不进行分组的SQL也可以使用聚合函数。 因此,聚合归并是在之前介绍的归并类的之上追加的归并能力,即装饰者模式。聚合函数可以归类为比较、累加和求平均值这3种类型。
比较类型的聚合函数是指MAX和MIN。它们需要对每一个同组的结果集数据进行比较,并且直接返回其最大或最小值即可。
累加类型的聚合函数是指SUM和COUNT。它们需要将每一个同组的结果集数据进行累加。
求平均值的聚合函数只有AVG。它必须通过SQL改写的SUM和COUNT进行计算。
结果归并从结构划分可分为:流式归并、内存归并和装饰者归并。流式归并和内存归并是互斥的,装饰者归并可以在流式归并和内存归并之上做进一步的处理。
内存归并 很容易理解,他是将所有分片结果集的数据都遍历并存储在内存中,再通过统一的分组、排序以及聚合等
计算之后,再将其封装成为逐条访问的数据结果集返回
流式归并 是指每一次从数据库结果集中获取到的数据,都能够通过游标逐条获取的方式返回正确的单条数据,它与
数据库原生的返回结果集的方式最为契合。遍历、排序以及流式分组都属于流式归并的一种。