Sharding JDBC 快速入门及简单使用(分片算法、SPI机制、执行流程)

685 阅读10分钟

1. Sharding-Sphere

Sharding-JDBC 最早是当当网内部使用的一款分库分表框架,到2017年的时候才开始对外开源,这几年在大量社区贡献者的不断迭代下,功能也逐渐完善,现已更名为 ShardingSphere ,2020年4⽉16⽇正式成为 Apache 软件基⾦会的顶级项⽬。

随着版本的不断更迭 ShardingSphere 的核心功能也变得多元化起来。如下图,ShardingSphere生态包含三款开源分布式数据库中间件解决方案,Sharding-JDBC、Sharding-Proxy、Sharding-Sidecar。

image-20220106131446670

Apache ShardingSphere 5.x 版本开始致力于提供可插拔架构,项目的功能组件能够灵活的以可插拔的方式进行扩展。 目前,数据分片、读写分离、数据加密、影子库压测等功能,以及对 MySQL、PostgreSQL、SQLServer、Oracle 等 SQL 与协议的支持,均通过插件的方式织入项目。 开发者能够像使用积木一样定制属于自己的独特系统。Apache ShardingSphere 目前已提供数十个 SPI 作为系统的扩展点,而且仍在不断增加中。

如图是Sharding-Sphere的整体架构。

image-20220106131554445

1.1 Sharding-JDBC

Sharding-JDBC是比较常用的一个组件,它定位的是一个增强版的JDBC驱动,简单来说就是在应用端来完成数据库分库分表相关的路由和分片操作,也是我们本次重点去分析的组件。

我们在项目内引入Sharding-JDBC的依赖,我们的业务代码在操作数据库的时候,就会通过Sharding-JDBC的代码连接到数据库。也就是分库分表的一些核心动作,比如SQL解析,路由,执行,结果处理,都是由它来完成的,它工作在客户端。

image-20220106131803398

1.2 Sharding-Proxy

Sharding-Proxy有点类似于Mycat,它是提供了数据库层面的代理,什么意思呢?简单来说,以前我们的应用是直连数据库,引入了Sharding-Proxy之后,我们的应用是直连Sharding-Proxy,然后Sharding-Proxy通过处理之后再转发到mysql中。

这种方式的好处在于,用户不需要感知到分库分表的存在,相当于正常访问mysql。目前Sharding-Proxy支持Mysql和PostgreSQL两种数据库协议,如下图所示。

image-20220106131855752

1.3 Sharding-Sidecar(TODO)

看到Sidecar,大家应该就能想到服务网格架构,它主要定位于 Kubernetes 的云原生数据库代理,以Sidecar 的形式代理所有对数据库的访问。目前Sharding-Sidecar还处于开发阶段未发布。

2. Sharding-JDBC

Sharding-JDBC是对原有JDBC驱动的增强,在分库分表的场景中,为应用提供了如下图所示的功能。

image-20220106132029248

2.1 实战

话不多说,现在我们知道了他有什么功能,是不是迫不及待想实现分库分表了?那就让我们直接进入实战吧。由于篇幅有限,我将实战分为JAVA API版本以及常用的Springboot版本。大家可以前往查看,相关概念问题还在本文阐述。当然,建议了解了相关概念后再去实战。

2.2 相关概念

前面我们通过两种方式演示了Sharding-JDBC的分库分表功能的用法,其实,从这个层面来说,Sharding-JDBC相当于增强了JDBC驱动的功能,使得开发者只需要通过配置就可以轻松完成分库分表功能的实现。

在Sharding-JDBC中,有一些表的概念,需要给大家普及一下,逻辑表、真实表、分片键、数据节点、动态表、广播表、绑定表。

2.2.1 逻辑表

逻辑表可以理解为数据库中的视图,是一张虚拟表。可以映射到一张物理表,也可以由多张物理表组成,这些物理表可以来自于不同的数据源。对于mysql, Hbase和ES,要组成一张逻辑表,只需要他们有相同含义的key即可。这个key在mysql中是主键,Hbase中是生成rowkey用的值,是ES中的key。

在前面的分库分表规则配置中,就有用到t_order这个逻辑表的定义,当我们针对t_order表操作时,会根据分片规则映射到实际的物理表进行相关事务操作,如下图所示,逻辑表会在SQL解析和路由时被替换成真实的表名。

spring.shardingsphere.rules.sharding.tables.t_order.actual-data-nodes=ds-$-> {0.}.t_order_$->{0.}

image-20220106133300107

2.2.2 广播表

广播表也叫全局表,也就是它会存在于多个库中冗余,避免跨库查询问题。

比如省份、字典等一些基础数据,为了避免分库分表后关联表查询这些基础数据存在跨库问题,所以可以把这些数据同步给每一个数据库节点,这个就叫广播表,如下图所示。

image-20220106133518923

在Sharding-JDBC中,配置方式如下

# 广播表, 其主节点是ds0 
spring.shardingsphere.sharding.broadcast-tables=t_config 
spring.shardingsphere.sharding.tables.t_config.actual-data-nodes=ds$-> {0}.t_config

2.2.3 绑定表

我们有些表的数据是存在逻辑的主外键关系的,比如订单表order_info,存的是汇总的商品数,商品金额;订单明细表order_detail,是每个商品的价格,个数等等。或者叫做从属关系,父表和子表的关系。他们之间会经常有关联查询的操作,如果父表的数据和子表的数据分别存储在不同的数据库,跨库关联查询也比较麻烦。所以我们能不能把父表和数据和从属于父表的数据落到一个节点上呢?

比如order_id=1001的数据在node1,它所有的明细数据也放到node1;order_id=1002的数据在node2,它所有的明细数据都放到node2,这样在关联查询的时候依然是在一个数据库,如下图所示:

image-20220106133842220

# 绑定表规则,多组绑定规则使用数组形式配置 
spring.shardingsphere.rules.sharding.binding-tables=t_order,t_order_item

如果存在多个绑定表规则,可以用数组的方式声明

spring.shardingsphere.rules.sharding.binding-tables[0]= # 绑定表规则列表 
spring.shardingsphere.rules.sharding.binding-tables[1]= # 绑定表规则列表 
spring.shardingsphere.rules.sharding.binding-tables[x]= # 绑定表规则列表

2.3 Sharding-JDBC中的分片

Sharding-JDBC内置了很多常用的分片策略,这些算法主要针对两个维度

  • 数据源分片
  • 数据表分片

Sharding-JDBC的分片策略包含了分片键和分片算法;

  • 分片键,用于分片的数据库字段,是将数据库(表)水平拆分的关键字段。例:将订单表中的订单主键的尾数取模分片,则订单主键为分片字段。 SQL中如果无分片字段,将执行全路由,性能较差。 除了对单分片字段的支持,ShardingSphere也支持根据多个字段进行分片。
  • 分片算法,就是用来实现分片的计算规则。

Sharding-JDBC提供内置了多种分片算法,包含四种类型分别是

  • 自动分片算法
  • 标准分片算法
  • 复合分片算法
  • Hinit分片算法

2.3.1 自动分片算法

自动分片算法,就是根据我们配置的算法表达式完成数据的自动分发功能,在Sharding-JDBC中提供了五种自动分片算法.

  • 取模分片算法
  • 哈希取模分片算法
  • 基于分片容量的范围分片算法
  • 基于分片边界的范围分片算法
  • 自动时间段分片算法

2.3.2 标准分片算法

标准分片策略( StandardShardingStrategy ),它只支持对单个分片健(字段)为依据的分库分表,Sharding-JDBC提供了两种算法实现。

2.3.3 复合分片算法

使用场景:SQL 语句中有 > , >= , <= , < , = , IN 和 BETWEEN AND 等操作符,不同的是复合分片策略支持对多个分片健操作。

2.3.4 自定义分片算法

除了默认提供了分片算法之外,我们可以根据实际需求自定义分片算法,Sharding-JDBC同样提供了几种类型的扩展实现:

  • 标准分片算法
  • 复合分片算法
  • Hinit分片策略
  • 不分片策略

3. 关于Java中的SPI机制

SPI 的全名为 Service Provider Interface,它的核心思想是中间件中定义标准,然后使用者可以在这个标准上实现自定义扩展,举个比较常见的例子,就是JDBC驱动。 Java官方只提供了JDBC驱动的接口:

java.sql.Driver

然后各大数据库厂商,如Mysql、Oracle都会基于这个接口定义不同数据库的连接实现,然后使用java语言的开发者不需要关心不同数据库的具体配置,只需要集成相关的依赖包以及配置相关驱动,Java程序就能自动匹配到相关的实现完成数据库连接。

这种思想在很多地方都有使用,比如Spring中的SpringFactoriesLoader、Dubbo中的SPI思想、Sentinel中的SPI思想等等。很多中间件中使用的SPI都不是Java原生的SPI,而是基于这种思想优化过后的。

下面来看一下SPI如何使用:

SPI的使用规则

SPI的使用必须遵循以下约定。

  • META/service的目录 放在classpath目录下

  • 扩展类全限定名组成的文件名

  • 文件内容填写该扩展类的实现类

  • ServiceLoad.load(Driver.class)

    • 会从classpath目录下 META-INF/service目录下找到java.sql.Driver的文件名 [list]
    • 解析这些文件
    • 得到所有文件中填写的实现类

4. Sharding-JDBC的工作流程

ShardingSphere 的 3 个产品的数据分片主要流程是完全一致的。 核心由 SQL 解析 => 执行器优化 => SQL 路由 => SQL 改写 => SQL 执行 => 结果归并 的流程组成,如下图所示。

image-20220107140312384

4.1 SQL解析引擎

先通过词法解析器将 SQL 拆分为一个个不可再分的单词。再使用语法解析器对 SQL 进行理解,并最终提炼出解析上下文。 解析上下文包括表、选择项、排序项、分组项、聚合函数、分页信息、查询条件以及可能需要修改的占位符的标记。

image-20220107140456254

为了便于理解,抽象语法树中的关键字的 Token 用绿色表示,变量的 Token 用红色表示,灰色表示需要进一步拆分。

最后,通过 visitor 对抽象语法树遍历构造域模型,通过域模型( SQLStatement )去提炼分片所需的上下文,并标记有可能需要改写的位置。 供分片使用的解析上下文包含查询选择项(Select Items)、表信息(Table)、分片条件(Sharding Condition)、自增主键信息(Auto increment Primary Key)、排序信息(Order By)、分组信息(Group By)以及分页信息(Limit、Rownum、Top)。 SQL 的一次解析过程是不可逆的,一个个 Token 按 SQL 原本的顺序依次进行解析,性能很高。 考虑到各种数据库 SQL 方言的异同,在解析模块提供了各类数据库的 SQL 方言字典。

4.2 路由引擎

路由引擎主要作用是,根据解析的上下文匹配数据库和表的分片策略,并生成路由路径。

这里的分片策略就是我们前面讲的内置分片策略或者用户自定义分片策略。内置的分片策略大致可分为尾数取模、哈希、范围、标签、时间等,用户方配置的分片策略则更加灵活,可以根据使用方需求定制复合分片策略。

对于携带分片键的 SQL,根据分片键的不同可以划分为单片路由(分片键的操作符是等号)、多片路由(分片键的操作符是 IN)和范围路由(分片键的操作符是 BETWEEN)。 不携带分片键的 SQL 则采用广播路由。

所谓广播路由,就是sql中没有携带分片键,此时需要扫描全部库和全部表进行数据聚合。

4.3 SQL改写引擎

我们在配置数据库的分片策略时,都是基于逻辑库和逻辑表来操作,包括我们编写的sql语句,都是面向逻辑库来操作。

这个基于逻辑库的查询,并不能真正在数据库中执行,比如:

SELECT order_id FROM t_order WHERE order_id=1;

此时t_order表示一个逻辑表,真正的表可能是db0中的t_order_0。所以需要通过SQL改写引擎把逻辑表改成真实数据库中可以执行的正确SQL。

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

SELECT order_id FROM t_order_1 WHERE order_id=1;

除了表名改写之外,改写引擎还提供了补列、分页修正、批量拆分等改写方式。

4.4 SQL执行引擎

ShardingSphere采用一套自动化的执行引擎,负责将路由和改写完成之后的真实SQL安全且高效发送到底层数据源执行。

4.5 结果归并引擎

将从各个数据节点获取的多数据结果集,组合成为一个结果集并正确的返回至请求客户端,称为结果归并。

ShardingSphere 支持的结果归并从功能上分为遍历、排序、分组、分页和聚合 5 种类型。