龙空技术网

5W 字详解分库分表之 Sharding-JDBC 中间件

程序猿久一 248

前言:

今天小伙伴们对“039oraclebi解决方案”大概比较重视,看官们都想要分析一些“039oraclebi解决方案”的相关文章。那么小编在网上汇集了一些关于“039oraclebi解决方案””的相关知识,希望姐妹们能喜欢,各位老铁们快快来学习一下吧!

本文大纲如下

Sharding-JDBC 的基本用法和基本原理

前言

1. 我的出生和我的家族

2. 我统治的世界和我的职责

3. 召唤我的方式

4. 我的特性和我的工作方法

4.3.1. SQL 解析

4.3.2. SQL 路由

4.3.3. SQL 改写

4.3.4. SQL 执行

4.3.5. 结果归并

4.2.1. 逻辑表和物理表

4.2.2. 分片键

4.2.3. 路由

4.2.4. 分片策略和分片算法

4.2.5. 绑定表

4.2. 一些核心概念

4.3. 我处理 SQL 的过程

5. 结束语

前言

这是一篇将“介绍 Sharding-JDBC 基本使用方法”作为目标的文章,但笔者却把大部分文字放在对 Sharding-JDBC 的工作原理的描述上,因为笔者认为原理是每个 IT 打工人学习技术的归途。

使用框架、中间件、数据库、工具包等公共组件来组装出应用系统是我们这一代 IT 打工人工作的常态。对于这些公共组件——比如框架——的学习,有些人的方法是这样的:避开复杂晦涩的框架原理,仅仅关注它的各种配置、API、注解,在尝试了这个框架的常用配置项、API、注解的效果之后,就妄称自己学会了这个框架。这种对技术的肤浅的认知既经不起实践的考验,也经不起面试官的考验,甚至连自己使用这些配置项、API、注解在干什么都没有明确的认知。

所以,打工人们,还是多学点原理,多看点源码,让优秀的设计思想、算法和编程风格冲击一下自己的大脑吧 :-)

因为 Sharding-JDBC 的设计细节实在太多,因此本文不可能对 Sharding-JDBC 进行面面俱到的讲解。笔者在本文中仅仅保留了对 Sharding-JDBC 的核心特性、核心原理的讲解,并尽量使用简单生动的文字进行表达,使读者阅读本文后对 Sharding-JDBC 的基本原理和使用有清晰的认知。为了使这些文字尽量摆脱枯燥的味道,文章采用了第一人称的讲述方式,让 Sharding-JDBC 现身说法,进行自我剖析,希望给大家一个更好的阅读体验。

但是,妄图不动脑子就能对某项技术产生深度认知是绝不可能的,你思考得越多,你得到的越多。这就印证了那句话:“我变秃了,也变强了。”

1. 我的出生和我的家族

我是 Sharding-JDBC,一个关系型数据库中间件,我的全名是 Apache ShardingSphere JDBC,我被冠以 Apache 这个贵族姓氏是 2020 年 4 月的事情,这意味着我进入了代码世界的“体制内”。但我还是喜欢别人称呼我的小名,Sharding-JDBC。

我的创造者在我诞生之后给我讲了我的身世:

你的诞生是一个必然的结果。

在你诞生之前,传统软件的存储层架构将所有的业务数据存储到单一数据库节点,在性能、可用性和运维成本这三方面已经难于满足互联网的海量数据场景。

从性能方面来说,由于关系型数据库大多采用 B+树类型的索引,在数据量逐渐增大的情况下,索引深度的增加也将使得磁盘访问的 IO 次数增加,进而导致查询性能的下降;同时,高并发访问请求也使得集中式数据库成为系统的最大瓶颈。

从可用性的方面来讲,应用服务器节点能够随意水平拓展(水平拓展就是增加应用服务器节点数量)以应对不断增加的业务流量,这必然导致系统的最终压力都落在数据库之上。而单一的数据库节点,或者简单的主从架构,已经越来越难以承担众多应用服务器节点的数据查询请求。数据库的可用性,已成为整个系统的关键。

从运维成本方面考虑,随着数据库实例中的数据规模的增大,DBA 的运维压力也会增加,因为数据备份和恢复的时间成本都将随着数据量的增大而愈发不可控。

这样看来关系型数据库似乎难以承担海量记录的存储。

然而,关系型数据库当今依然占有巨大市场,是各个公司核心业务的基石。在传统的关系型数据库无法满足互联网场景需要的情况下,将数据存储到原生支持分布式的 NoSQL 的尝试越来越多。但 NoSQL 对 SQL 的不兼容性以及生态圈的不完善,使得它们在与关系型数据库的博弈中处于劣势,关系型数据库的地位却依然不可撼动,未来也难于撼动。

我们目前阶段更加关注在原有关系型数据库的基础上做增量,使之更好适应海量数据存储和高并发查询请求的场景,而不是要颠覆关系型数据库。

分库分表方案就是这种增量,它的诞生解决了海量数据存储和高并发查询请求的问题。

但是,单一数据库被分库分表之后,繁杂的库和表使得编写持久层代码的工程师的思维负担翻了很多倍,他们需要考虑一个业务 SQL 应该去哪个库的哪个表里去查询,查询到的结果还要进行聚合,如果遇到多表关联查询、排序、分页、事务等等问题,那简直是一个噩梦。

于是我们创造了你。你可以让工程师们以像查询单数据库实例和单表那样来查询被水平分割的库和表,我们称之为透明查询。

你是水平分片世界的神。

这使我感到骄傲。

我被定位为一个轻量级 Java 框架,我在 Java 的 JDBC 层提供的额外服务,可以说是一个增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。

我适用于任何基于 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC。

我支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP 等。

我支持任意实现 JDBC 规范的数据库,目前支持 MySQL,Oracle,SQLServer,PostgreSQL 以及任何遵循 SQL92 标准的数据库。

我的创造者起初只创造了我一个独苗,后来为了我的家族的兴盛,我的两个兄弟——Apache ShardingSphere Proxy、Apache ShardingSphere Sidecar 又被创造了出来。前者被定位为透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本,⽤于完成对异构语⾔的支持;后者被定位为 Kubernetes 的云原⽣数据库代理,以 Sidecar 的形式代理所有对数据库的访问。通过无中心、零侵⼊的⽅案提供与数据库交互的的啮合层,即 Database Mesh,又可称数据库⽹格。

因此,我们这个家族叫做 Apache ShardingSphere,旨在在分布式的场景下更好利用关系型数据库的计算和存储能力,而并非实现一个全新的关系型数据库。我们三个既相互独立,又能配合使用,均提供标准化的数据分片、分布式事务和数据库治理功能。

2. 我统治的世界和我的职责

我是 Sharding-JDBC,我生活在一个数据水平分片的世界,我统治着这个世界里被水平拆分后的数据库和表。

在分片的世界里,数据分片有两种法则:垂直拆分和水平拆分。

按照业务拆分的方式称为垂直分片,又称为纵向拆分,它的核心理念是专库专用。在拆分之前,一个数据库由多个数据表构成,每个表对应着不同的业务。而拆分之后,则是按照业务将表进行归类,分布到不同的数据库中,从而将压力分散至不同的数据库。下图展示了根据业务需要,将用户表和订单表垂直分片到不同的数据库的方案。

垂直分片往往需要对架构和设计进行调整。通常来讲,是来不及应对互联网业务需求快速变化的;而且,它也并无法真正的解决单点瓶颈。如果垂直拆分之后,表中的数据量依然超过单节点所能承载的阈值,则需要水平分片来进一步处理。

水平分片又称为横向拆分。相对于垂直分片,它不再将数据根据业务逻辑分类,而是通过某个字段(或某几个字段),根据某种规则将数据分散至多个库或表中,每个分片仅包含数据的一部分。例如:根据主键分片,偶数主键的记录放入 0 库(或表),奇数主键的记录放入 1 库(或表),如下图所示。

水平分片从理论上突破了单机数据量处理的瓶颈,并且扩展相对自由,是分库分表的标准解决方案。我管辖的就是水平分片世界。

通过分库和分表进行数据的拆分来使得各个表的数据量保持在阈值以下,是应对高并发和海量数据系统的有效手段。此外,使用多主多从的分片方式,可以有效的避免数据单点,从而提升数据架构的可用性。

其实,水平分库本质上还是在分表,因为被水平拆分后的库中,都有相同的表分片。

分库和分表这项工作并不是我来做,我虽然是神,但我还没有神到能理解你们这些工程师的业务设计和架构设计,从而自动把你们的业务数据库和业务表进行分片。对哪部分进行分片、怎样分片、分多少份,这些工作全部由这些工程师进行。当这些分库分表的工作被完成后,你们只需要在我的配置文件中或者通过我的 API 告诉我这些拆分规则(这就是后文要提到的分片策略)即可,剩下的事情,交给我去做。

我是 Sharding-JDBC,我的职责是尽量透明化水平分库分表所带来的影响,让使用方尽量像使用一个数据库一样使用水平分片之后的数据库集群,或者像使用一个数据表一样使用水平分片之后的数据表。由于我的治理,每个服务器节点只能看到一个逻辑上的数据库节点,和其中的多个逻辑表,它们看不到真正存在于物理世界中的被水平分割的多个数据库分片和被水平分割的多个数据表分片。服务器节点看到的简单的持久层结构,其实是我苦心营造的幻象。

而为了营造这种幻象,我在幕后付出了很多。

当一个 Java 应用服务器节点将一个查询 SQL 交给我之后,我要做下面几件事:

1)SQL 解析:解析分为词法解析和语法解析。我先通过词法解析器将这句 SQL 拆分为一个个不可再分的单词,再使用语法解析器对 SQL 进行理解,并最终提炼出解析上下文。简单来说就是我要理解这句 SQL,明白它的构造和行为,这是下面的优化、路由、改写、执行和归并的基础。

2)SQL 路由:我根据解析上下文匹配用户对这句 SQL 所涉及的库和表配置的分片策略(关于用户配置的分片策略,我后文会慢慢解释),并根据分片策略生成路由后的 SQL。路由后的 SQL 有一条或多条,每一条都对应着各自的真实物理分片。

3)SQL 改写:我将 SQL 改写为在真实数据库中可以正确执行的语句(逻辑 SQL 到物理 SQL 的映射,例如把逻辑表名改成带编号的分片表名)。

4)SQL 执行:我通过多线程执行器异步执行路由和改写之后得到的 SQL 语句。

5)结果归并:我将多个执行结果集归并以便于通过统一的 JDBC 接口输出。

如果你连读这段工作流程都很困难,那你就能明白我在这个水平分片的世界里有多辛苦。关于这段工作流程,我会在后文慢慢说给你听。

3. 召唤我的方式

我是 Sharding-JDBC,我被定位为一个轻量级数据库中间件,当你们召唤我去统治水平拆分后的数据库和数据表时,只需要做下面几件事:

1)引入依赖包。

maven 是统治依赖包世界的神,在他诞生之后,一切对 jar 包的引用就变得简单了。向 maven 获取我的 jar 包,咒语是:

<dependency>

<groupId>org.apache.shardingsphere</groupId>

<artifactId>shardingsphere-jdbc-core</artifactId>

<version>${latest.release.version}</version>

</dependency>

于是,我就出现在了这个项目中!

如果你们构建的项目已经被 Springboot 统治了(Springboot 是 Spring 的继任者,Spring 是统治对象世界的神,Springboot 继承了 Spring 的统治法则,并简化了 Spring 的配置),那么就可以向 maven 获取我的 springboot starter jar 包,咒语是:

<dependency>

<groupId>org.apache.shardingsphere</groupId>

<artifactId>shardingsphere-jdbc-spring-boot-starter</artifactId>

<version>${shardingsphere.version}</version>

</dependency>

这样,我就能和 Springboot 神共存于同一个项目。

2)进行水平分片规则配置。

你们要把水平分片规则配置告诉我,这样我才能知道你们是怎样水平拆分数据库和数据表的。你们可以通过我提供的 Java API,或者配置文件告诉我分片规则。

如果是以 Java API 的方式进行配置,示例如下:

// 配置真实数据源

Map<String, DataSource> dataSourceMap = new HashMap<>;

// 配置第 1 个数据源

BasicDataSource dataSource1 = new BasicDataSource;

dataSource1.setDriverClassName("com.mysql.jdbc.Driver");

dataSource1.setUrl("jdbc:mysql://localhost:3306/ds0");

dataSource1.setUsername("root");

dataSource1.setPassword("");

dataSourceMap.put("ds0", dataSource1);

// 配置第 2 个数据源

BasicDataSource dataSource2 = new BasicDataSource;

dataSource2.setDriverClassName("com.mysql.jdbc.Driver");

dataSource2.setUrl("jdbc:mysql://localhost:3306/ds1");

dataSource2.setUsername("root");

dataSource2.setPassword("");

dataSourceMap.put("ds1", dataSource2);

// 配置 t_order 表规则

ShardingTableRuleConfiguration orderTableRuleConfig

= new ShardingTableRuleConfiguration(

"t_order",

"ds${0..1}.t_order${0..1}"

);

// 配置 t_order 被拆分到多个子库的策略

orderTableRuleConfig.setDatabaseShardingStrategy(

new StandardShardingStrategyConfiguration(

"user_id",

"dbShardingAlgorithm"

)

);

// 配置 t_order 被拆分到多个子表的策略

orderTableRuleConfig.setTableShardingStrategy(

new StandardShardingStrategyConfiguration(

"order_id",

"tableShardingAlgorithm"

)

);

// 省略配置 t_order_item 表规则...

// ...

// 配置分片规则

ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration;

shardingRuleConfig.getTables.add(orderTableRuleConfig);

// 配置 t_order 被拆分到多个子库的算法

Properties dbShardingAlgorithmrProps = new Properties;

dbShardingAlgorithmrProps.setProperty(

"algorithm-expression",

"ds${user_id % 2}"

);

shardingRuleConfig.getShardingAlgorithms.put(

"dbShardingAlgorithm",

new ShardingSphereAlgorithmConfiguration("INLINE", dbShardingAlgorithmrProps)

);

// 配置 t_order 被拆分到多个子表的算法

Properties tableShardingAlgorithmrProps = new Properties;

tableShardingAlgorithmrProps.setProperty(

"algorithm-expression",

"t_order${order_id % 2}"

);

shardingRuleConfig.getShardingAlgorithms.put(

"tableShardingAlgorithm",

new ShardingSphereAlgorithmConfiguration("INLINE", tableShardingAlgorithmrProps)

);

这段配置代码中涉及的 t_order 表(存储订单的基本信息)的表结构为:

这段配置代码描述了对 t_order 表进行的如下图所示的数据表水平分片(对 t_order_item 表也要进行类似的水平分片,但是这部分配置省略了):

在这段配置中,或许你们注意到了一些奇怪的表达式:

ds$->{0..1}.t_order$->{0..1}

ds_${user_id % 2}

t_order_${order_id % 2}

这些表达式被称为 Groovy 表达式,它们的含义很容易识别:

1)对 t_order 进行两种维度的拆分:数据库维度和表维度数;

2)在数据库维度,t_order.user_id % 2 == 0 的记录全部落到 ds0,t_order.user_id % 2 == 1 的记录全部落到 ds1;(有人称这一过程为水平分库,其实它的本质还是在水平地分表,只不过依据表中 user_id 的不同把拆分的后的表放入两个数据库实例。)

3)在表维度,t_order.order_id% 2 == 0 的记录全部落到 t_order0,t_order.order_id% 2 == 1 的记录全部落到 t_order1。

4)对记录的读和写都按照这种方向进行,“方向”,就是分片方式,就是路由。

我允许你们这些工程师使用这种简洁的 Groovy 表达式告诉我你们设置的分片策略和分片算法。但是这种方式所能表达的含义是有限的。因此,我提供了分片策略接口和分片算法接口让你们利用 Java 代码尽情表达更为复杂的分片策略和分片算法。关于这一点,我将在《我的特性和工作方法》这一章详述。

而且在这里我要先告诉你,分片算法是分片策略的组成部分,分片策略设置=分片键设置+分片算法设置。上述配置里使用的策略是 Inline 类型的分片策略,使用的算法是 Inline 类型的行表达式算法,你或许不清楚我现在讲的这些术语,不要着急,我会在《我的特性和工作方法》这一章详述。

如果是以配置文件的方式进行配置,示例如下(这里以我的 springboot starter 包的 properties 配置文件为例):

# 配置真实数据源

spring.shardingsphere.datasource.names=ds0,ds1

# 配置第 1 个数据源

spring.shardingsphere.datasource.ds0.type=org.apache.commons.dbcp2.BasicDataSource

spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver

spring.shardingsphere.datasource.ds0.url=jdbc:mysql://localhost:3306/ds0

spring.shardingsphere.datasource.ds0.username=root

spring.shardingsphere.datasource.ds0.password=

# 配置第 2 个数据源

spring.shardingsphere.datasource.ds1.type=org.apache.commons.dbcp2.BasicDataSource

spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver

spring.shardingsphere.datasource.ds1.url=jdbc:mysql://localhost:3306/ds1

spring.shardingsphere.datasource.ds1.username=root

spring.shardingsphere.datasource.ds1.password=

# 配置 t_order 表规则

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

# 配置 t_order 被拆分到多个子库的策略

spring.shardingsphere.rules.sharding.tables.t_order.database-strategy.standard.sharding-column=user_id

spring.shardingsphere.rules.sharding.tables.t_order.database-strategy.standard.sharding-algorithm-name=database_inline

# 配置 t_order 被拆分到多个子表的策略

spring.shardingsphere.rules.sharding.tables.t_order.table-strategy.standard.sharding-column=order_id

spring.shardingsphere.rules.sharding.tables.t_order.table-strategy.standard.sharding-algorithm-name=table_inline

# 省略配置 t_order_item 表规则...

# ...

# 配置 t_order 被拆分到多个子库的算法

spring.shardingsphere.rules.sharding.sharding-algorithms.database_inline.type=INLINE

spring.shardingsphere.rules.sharding.sharding-algorithms.database_inline.props.algorithm-expression=ds_${user_id % 2}

# 配置 t_order 被拆分到多个子表的算法

spring.shardingsphere.rules.sharding.sharding-algorithms.table_inline.type=INLINE

spring.shardingsphere.rules.sharding.sharding-algorithms.table_inline.props.algorithm-expression=t_order_${order_id % 2}

这段配置文件的语义和上面的 Java 配置代码同义。

3)创建数据源。

若使用上文所示的 Java API 进行配置,则可以通过 ShardingSphereDataSourceFactory 工厂创建数据源,该工厂产生一个 ShardingSphereDataSource 实例,ShardingSphereDataSource 实现自 JDBC 的标准接口 DataSource(所以 ShardingSphereDataSource 实例也是接口 DataSource 的实例)。之后,就可以通过 dataSource 调用原生 JDBC 接口来执行 SQL 查询,或者将 dataSource 配置到 JPA,MyBatis 等 ORM 框架来执行 SQL 查询。

// 创建 ShardingSphereDataSource

DataSource dataSource = ShardingSphereDataSourceFactory.createDataSource(

dataSourceMap,

Collections.singleton(shardingRuleConfig, new Properties)

);

若使用上文所示的基于 springboot starter 的 properties 配置文件进行分片配置,则可以直接通过 Spring 提供的自动注入的方式获得数据源实例 dataSource(同样,这也是一个 ShardingSphereDataSource 实例)。之后,就可以通过 dataSource 调用原生 JDBC 接口来执行 SQL 查询,或者将 dataSource 配置到 JPA,MyBatis 等 ORM 框架来执行 SQL 查询。

/**

* 注入一个 ShardingSphereDataSource 实例

*/

@Resource

private DataSource dataSource;

有了 dataSource(以上两种方式产生的 dataSource 没有区别,都是 ShardingSphereDataSource 的一个实例,业务代码将 SQL 交给这个 dataSource,也就是交到了我的手中),就可以执行 SQL 查询了。

4)执行 SQL。这里给出 dataSource 调用原生 JDBC 接口来执行 SQL 查询的示例:

String sql = "SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.user_id=? AND o.order_id=?";

try (

Connection conn = dataSource.getConnection;

PreparedStatement ps = conn.prepareStatement(sql)

) {

ps.setInt(1, 10);

ps.setInt(2, 1000);

try (

ResultSet rs = preparedStatement.executeQuery

) {

while(rs.next) {

// ...

}

}

}

在这个示例中,Java 代码调用 dataSource 的 JDBC 接口时,只感觉自己在对一个逻辑库中的两个逻辑表进行关联查询,并没有意识到物理分片的存在。而背后是我在进行 SQL 语句的解析、路由、改写、执行和结果归并!

4. 我的特性和我的工作方法4.2. 一些核心概念

我是 Sharding-JDBC,我是统治水平分片世界的神,我要向你们解释我的特性和治理方法。在此之前,我要给出一系列用于描述我的术语。

4.2.1. 逻辑表和物理表

例如,订单表根据主键尾数被水平拆分为 10 张表,分别是 t_order0 到 t_order9,它们的逻辑表名为 t_order,而 t_order0 到 t_order9 就是物理表。

4.2.2. 分片键

例如,若根据订单表中的订单主键的尾数取模结果进行水平分片,则订单主键为分片键。订单表既可以根据单个分片键进行分片,也同样可以根据多个分片键(例如 order_id 和 user_id)进行分片。

4.2.3. 路由

应用程序服务器将针对逻辑表编写的 SQL 交给我,我在执行前,要找到 SQL 语句里包含的查询条件(where ......)所对应的分片(物理表),然后再针对这些分片进行查询,这个找分片的过程叫做路由。

而怎样找分片,是由你们在分片策略中告诉我的。

4.2.4. 分片策略和分片算法

在上文的配置示例中,有如下的一段:

......

// 配置 t_order 被拆分到多个子库的策略

orderTableRuleConfig.setDatabaseShardingStrategy(

new StandardShardingStrategyConfiguration(

"user_id",

"dbShardingAlgorithm"

)

);

// 配置 t_order 被拆分到多个子表的策略

orderTableRuleConfig.setTableShardingStrategy(

new StandardShardingStrategyConfiguration(

"order_id",

"tableShardingAlgorithm"

)

);

......

ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration;

shardingRuleConfig.getTables.add(orderTableRuleConfig);

// 配置 t_order 被拆分到多个子库的算法

Properties dbShardingAlgorithmrProps = new Properties;

dbShardingAlgorithmrProps.setProperty(

"algorithm-expression",

"ds${user_id % 2}"

);

shardingRuleConfig.getShardingAlgorithms.put(

"dbShardingAlgorithm",

new ShardingSphereAlgorithmConfiguration("INLINE", dbShardingAlgorithmrProps)

);

// 配置 t_order 被拆分到多个子表的算法

Properties tableShardingAlgorithmrProps = new Properties;

tableShardingAlgorithmrProps.setProperty(

"algorithm-expression",

"t_order${order_id % 2}"

);

shardingRuleConfig.getShardingAlgorithms.put(

"tableShardingAlgorithm",

new ShardingSphereAlgorithmConfiguration("INLINE", tableShardingAlgorithmrProps)

);

......

它们表达的就是对 t_order 表进行的分片策略和分片算法的配置。

上文说到,我允许你们这些工程师使用简洁的 Groovy 表达式告诉我你们设置的分片策略和分片算法。但是这种方式所能表达的含义是有限的。因此,我提供了分片策略接口和分片算法接口让你们利用灵活的 Java 代码尽情表达更为复杂的分片策略和分片算法。

所谓分片策略,就是分片键和分片算法的组合,由于分片算法的独立性,我将其独立抽离出来,由你们自己实现,也就是告诉我数据是怎么根据分片键的值找到对应的分片,进而对这些分片执行 SQL 查询。

当然我也提供了一些内置的简单算法的实现。上面提到的基于 Groovy 表达式的分片算法就是我内置的一种算法实现,你们只要给我一段语义准确无误的 Groovy 表达式,我就能知道怎么根据分片键的值找到对应的分片。

我的分片策略有两个维度,如下图所示,分别是数据源分片策略(databaseShardingStrategy)和表分片策略(tableShardingStrategy)。数据源分片策略表示数据被路由到目标物理数据库的策略,表分片策略表示数据被路由到目标物理表的策略。表分片策略是依赖于数据源分片策略的,也就是说要先分库再分表,当然也可以只分表。

我目前可以提供如下几种分片(无论是对库分片还是对表分片)策略:标准分片策略(使用精确分片算法或者范围分片算法)、复合分片策略(使用符合分片算法)、Hint 分片策略(使用 Hint 分片算法)、Inline 分片策略(使用 Grovvy 表达式作为分片算法)、不分片策略(不使用分片算法)。

我的 Jar 包源码里的策略类和算法接口如下:

一、标准分片策略

标准分片策略 StandardShardingStrategy 的源代码(部分)如下,这是一个 final class。

package org.apache.shardingsphere.core.strategy.route.standard;

......

public final classStandardShardingStrategyimplementsShardingStrategy{

private final String shardingColumn;

/**

* 要配合 PreciseShardingAlgorithm 或 RangeShardingAlgorithm 使用

* 标准分片策略

*/

private final PreciseShardingAlgorithm preciseShardingAlgorithm;

private final RangeShardingAlgorithm rangeShardingAlgorithm;

publicStandardShardingStrategy(

// 传入分片配置

final StandardShardingStrategyConfiguration standardShardingStrategyConfig

) {

......

// 从配置中提取分片键

shardingColumn = standardShardingStrategyConfig.getShardingColumn;

// 从配置中提取分片算法

preciseShardingAlgorithm = standardShardingStrategyConfig.getPreciseShardingAlgorithm;

rangeShardingAlgorithm = standardShardingStrategyConfig.getRangeShardingAlgorithm;

}

@Override

public Collection<String> doSharding(

// 所有可能的分片表(或分片库)名称

final Collection<String> availableTargetNames,

// 分片键的值

final Collection<RouteValue> shardingValues,

final ConfigurationProperties properties

) {

RouteValue shardingValue = shardingValues.iterator.next;

Collection<String> shardingResult

= shardingValue instanceof ListRouteValue

// 处理精确分片

? doSharding(availableTargetNames, (ListRouteValue) shardingValue)

// 处理范围分片

: doSharding(availableTargetNames, (RangeRouteValue) shardingValue);

Collection<String> result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);

result.addAll(shardingResult);

// 根据分片键的值,找到对应的分片表(或分片库)名称并返回

return result;

}

/**

* 处理范围分片

*/

@SuppressWarnings("unchecked")

private Collection<String> doSharding(

// 所有可能的分片表(或分片库)名称

final Collection<String> availableTargetNames,

// 分片键的值

final RangeRouteValue<?> shardingValue

) {

......

// 调用 rangeShardingAlgorithm.doSharding根据分片键的值找到对应的

// 分片表(或分片库)名称并返回,rangeShardingAlgorithm.doSharding

// 由你们自己实现

return rangeShardingAlgorithm.doSharding(

availableTargetNames,

new RangeShardingValue(

shardingValue.getTableName,

shardingValue.getColumnName,

shardingValue.getValueRange

)

);

}

/**

* 处理精确分片

*/

@SuppressWarnings("unchecked")

private Collection<String> doSharding(

// 所有可能的分片表(或分片库)名称

final Collection<String> availableTargetNames,

// 分片键的值

final ListRouteValue<?> shardingValue

) {

Collection<String> result = new LinkedList<>;

for (Comparable<?> each : shardingValue.getValues) {

// 调用 preciseShardingAlgorithm.doSharding根据分片键的值找到对应的

// 分片表(或分片库)名称并返回,preciseShardingAlgorithm.doSharding

// 由你们自己实现

String target

= preciseShardingAlgorithm.doSharding(

availableTargetNames,

new PreciseShardingValue(

shardingValue.getTableName,

shardingValue.getColumnName,

each

)

);

if ( != target) {

result.add(target);

}

}

return result;

}

/**

* 获取所有的分片键

*/

@Override

public Collection<String> getShardingColumns {

Collection<String> result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);

result.add(shardingColumn);

return result;

}

}

其中 PreciseShardingAlgorithm(接口)和 RangeShardingAlgorithm(接口)的源代码分别为:

package org.apache.shardingsphere.api.sharding.standard;

......

public interface PreciseShardingAlgorithm<T extends Comparable<?>>

extends ShardingAlgorithm {

/**

* @param 所有可能的分片表(或分片库)名称

* @param 分片键的值

* @return 根据分片键的值,找到对应的分片表(或分片库)名称并返回

*/

String doSharding(

Collection<String> availableTargetNames,

PreciseShardingValue<T> shardingValue

);

}

package org.apache.shardingsphere.api.sharding.standard;

......

public interface RangeShardingAlgorithm<T extends Comparable<?>>

extends ShardingAlgorithm {

/**

* @param 所有可能的分片表(或分片库)名称

* @param 分片键的值

* @return 根据分片键的值,找到对应的分片表(或分片库)名称并返回

*/

Collection<String> doSharding(

Collection<String> availableTargetNames,

RangeShardingValue<T> shardingValue

);

}

标准分片策略提供对 SQL 语句中的操作符 =、>、 <、>=、<=、IN 和 BETWEEN AND 的分片操支持。

标准分片策略只支持单分片键,例如对 t_order 表只根据 order_id 分片。标准分片策略提供 PreciseShardingAlgorithm(接口)和 RangeShardingAlgorithm(接口)两个分片算法。PreciseShardingAlgorithm(接口)顾名思义用于处理操作符=和 IN 的精确分片。RangeShardingAlgorithm (接口)顾名思义用于处理操作符 BETWEEN AND、>、<、>=、<= 的范围分片。

我举个例子帮助你理解以上两段话的含义。以 t_order 为例,假如你使用 order_id 作为 t_order 的分片键,并设计了以下的分片策略:

策略一:设置 6 个分片

t_order.order_id % 6 == 0 的查询分片到 t_order0

t_order.order_id % 6 == 1 的查询分片到 t_order1

t_order.order_id % 6 == 2 的查询分片到 t_order2

t_order.order_id % 6 == 3 的查询分片到 t_order3

t_order.order_id % 6 == 4 的查询分片到 t_order4

t_order.order_id % 6 == 5 的查询分片到 t_order5

策略二:设置 2 个分片

t_order.order_id % 6 in (0,2,4) 的查询分片到 t_order1

t_order.order_id % 6 in (1,3,5) 的查询分片到 t_order1

策略三:经过估算订单不超过 60000 个,设置 6 个分片

t_order.order_id between 0 and 10000 的查询分片到 t_order0

t_order.order_id between 10000 and 20000 的查询分片到 t_order1

t_order.order_id between 20000 and 30000 的查询分片到 t_order2

t_order.order_id between 30000 and 40000 的查询分片到 t_order3

t_order.order_id between 40000 and 50000 的查询分片到 t_order4

t_order.order_id between 50000 and 60000 的查询分片到 t_order5

策略四:经过估算订单不超过 20000 个,设置 2 个分片

t_order.order_id <=10000 的查询分片到 t_order0

t_order.order_id >10000 的查询分片到 t_order1

......

那你就可以把以下三项:

1)分片键 order_id

2)描述以上分片策略内容的 PreciseShardingAlgorithm(接口)的实现类或 RangeShardingAlgorithm(接口)的实现类

3)前两项(即分片策略)的作用目标 t_order 表

写到分片配置里(无论是通过配置 API 还是通过配置文件),那我就能知道如何去路由 SQL,即根据分片键的值,找到对应的分片表(或分片库)。

有了这些配置,我就能帮你们透明处理如下 SQL 语句,不管实际的物理分片是怎样的:

-- 注:使用 t_order.order_id 作为 t_order 表的分片键

SELECT o.* FROM t_order o WHERE o.order_id = 10;

SELECT o.* FROM t_order o WHERE o.order_id IN (10, 11);

SELECT o.* FROM t_order o WHERE o.order_id > 10;

SELECT o.* FROM t_order o WHERE o.order_id <= 11;

SELECT o.* FROM t_order o WHERE o.order_id BETWEEN 10 AND 12;

......

INSERT INTO t_order(order_id, user_id) VALUES (20, 1001);

......

DELETE FROM t_order o WHERE o.order_id = 10;

DELETE FROM t_order o WHERE o.order_id IN (10, 11);

DELETE FROM t_order o WHERE o.order_id > 10;

DELETE FROM t_order o WHERE o.order_id <= 11;

DELETE FROM t_order o WHERE o.order_id BETWEEN 10 AND 12;

......

UPDATE t_order o SET o.update_time = NOW WHERE o.order_id = 10;

......

二、复合分片策略

复合分片策略 ComplexShardingStrategy 的源代码(部分)如下,这是一个 final class。

package org.apache.shardingsphere.core.strategy.route.complex;

......

public final classComplexShardingStrategyimplementsShardingStrategy{

@Getter

private final Collection<String> shardingColumns;

/**

* 要配合 ComplexKeysShardingAlgorithm 使用复合分片策略

*/

private final ComplexKeysShardingAlgorithm shardingAlgorithm;

publicComplexShardingStrategy(

// 传入分片配置

final ComplexShardingStrategyConfiguration complexShardingStrategyConfig

) {

......

// 从配置中提取分片键

shardingColumns = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);

shardingColumns.addAll(

Splitter

.on(",")

.trimResults

.splitToList(complexShardingStrategyConfig.getShardingColumns)

);

// 从配置中提取分片算法

shardingAlgorithm = complexShardingStrategyConfig.getShardingAlgorithm;

}

@SuppressWarnings("unchecked")

@Override

public Collection<String> doSharding(

// 所有可能的分片表(或分片库)名称

final Collection<String> availableTargetNames,

// 分片键的值

final Collection<RouteValue> shardingValues,

final ConfigurationProperties properties

) {

Map<String, Collection<Comparable<?>>> columnShardingValues

= new HashMap<>(shardingValues.size, 1);

Map<String, Range<Comparable<?>>> columnRangeValues

= new HashMap<>(shardingValues.size, 1);

String logicTableName = "";

// 提取多个分片键的值

for (RouteValue each : shardingValues) {

if (each instanceof ListRouteValue) {

columnShardingValues.put(

each.getColumnName,

((ListRouteValue) each).getValues

);

} else if (each instanceof RangeRouteValue) {

columnRangeValues.put(

each.getColumnName,

((RangeRouteValue) each).getValueRange

);

}

logicTableName = each.getTableName;

}

Collection<String> shardingResult

// 调用 shardingAlgorithm.doSharding根据分片键的值找到对应的

// 分片表(或分片库)名称并返回,shardingAlgorithm.doSharding

// 由你们自己实现

= shardingAlgorithm.doSharding(

availableTargetNames,

new ComplexKeysShardingValue(

logicTableName,

columnShardingValues,

columnRangeValues)

);

Collection<String> result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);

result.addAll(shardingResult);

// 根据分片键的值,找到对应的分片表(或分片库)名称并返回

return result;

}

}

其中 ComplexKeysShardingAlgorithm(接口)的源代码为:

package org.apache.shardingsphere.api.sharding.complex;

......

public interface ComplexKeysShardingAlgorithm<T extends Comparable<?>>

extends ShardingAlgorithm {

/**

* @param 所有可能的分片表(或分片库)名称

* @param 分片键的值

* @return 根据分片键的值,找到对应的分片表(或分片库)名称并返回

*/

Collection<String> doSharding(

Collection<String> availableTargetNames,

ComplexKeysShardingValue<T> shardingValue

);

}

复合分片策略提供对 SQL 语句中的操作符 =、>、<、>=、<=、IN 和 ETWEEN AND 的分片操作支持。

复合分片策略支持多分片键,例如对 t_order 表根据 order_id 和 user_id 分片。复合分片策略提供 ComplexKeysShardingAlgorithm(接口)分片算法。

我举个例子帮助你理解以上两段话的含义。以 t_order 为例,假如你使用 order_id 和 user_id 作为 t_order 的分片键,并设计了以下的分片策略:

策略一:设置 4 个分片

t_order.order_id % 2 == 0 && t_order.user_id % 2 == 0 的查询分片到 t_order0

t_order.order_id % 2 == 0 && t_order.user_id % 2 == 1 的查询分片到 t_order1

t_order.order_id % 2 == 1 && t_order.user_id % 2 == 0 的查询分片到 t_order2

t_order.order_id % 2 == 1 && t_order.user_id % 2 == 1 的查询分片到 t_order3

策略二:经过估算订单不超过 60000 个、用户不超过 1000 个,设置 4 个分片

t_order.order_id between 0 and 40000 && t_order.user_id between 0 and 500 的查询分片到 t_order0

t_order.order_id between 0 and 40000 && t_order.user_id between 500 and 1000 的查询分片到 t_order1

t_order.order_id between 40000 and 60000 && t_order.user_id between 0 and 500 的查询分片到 t_order2

t_order.order_id between 40000 and 60000 && t_order.user_id between 500 and 1000 的查询分片到 t_order3

......

那你就可以把以下三项:

1)分片键 order_id 和 user_id

2)描述以上分片策略内容的 ComplexKeysShardingAlgorithm(接口)的实现类

3)前两项(即分片策略)的作用目标 t_order 表

写到分片配置里(无论是通过配置 API 还是通过配置文件),那我就能知道如何去路由 SQL,即根据分片键的值,找到对应的分片表(或分片库)。

有了这些配置,我就能帮你们透明处理如下 SQL 语句,不管实际的物理分片是怎样的:

-- 注:使用 t_order.order_id、t_order.user_id 作为 t_order 表的分片键

SELECT o.* FROM t_order o WHERE o.order_id = 10;

SELECT o.* FROM t_order o WHERE o.order_id IN (10, 11);

SELECT o.* FROM t_order o WHERE o.order_id > 10;

SELECT o.* FROM t_order o WHERE o.order_id <= 11;

SELECT o.* FROM t_order o WHERE o.order_id BETWEEN 10 AND 12;

......

INSERT INTO t_order(order_id, user_id) VALUES (20, 1001);

......

DELETE FROM t_order o WHERE o.order_id = 10;

DELETE FROM t_order o WHERE o.order_id IN (10, 11);

DELETE FROM t_order o WHERE o.order_id > 10;

DELETE FROM t_order o WHERE o.order_id <= 11;

DELETE FROM t_order o WHERE o.order_id BETWEEN 10 AND 12;

......

UPDATE t_order o SET o.update_time = NOW WHERE o.order_id = 10;

......

SELECT o.* FROM t_order o WHERE o.order_id = 10 AND user_id = 1001;

SELECT o.* FROM t_order o WHERE o.order_id IN (10, 11) AND user_id IN (......);

SELECT o.* FROM t_order o WHERE o.order_id > 10 AND user_id > 1000;

SELECT o.* FROM t_order o WHERE o.order_id <= 11 AND user_id <= 1000;

SELECT o.* FROM t_order o WHERE (o.order_id BETWEEN 10 AND 12) AND (o.user_id BETWEEN 1000 AND 2000);

......

INSERT INTO t_order(order_id, user_id) VALUES (21, 1002);

......

DELETE FROM t_order o WHERE o.order_id = 10 AND user_id = 1001;

DELETE FROM t_order o WHERE o.order_id IN (10, 11) AND user_id IN (......);

DELETE FROM t_order o WHERE o.order_id > 10 AND user_id > 1000;

DELETE FROM t_order o WHERE o.order_id <= 11 AND user_id <= 1000;

DELETE FROM t_order o WHERE (o.order_id BETWEEN 10 AND 12) AND (o.user_id BETWEEN 1000 AND 2000);

......

UPDATE t_order o SET o.update_time = NOW WHERE o.order_id = 10 AND user_id = 1001;

......

注:在《召唤我的方式》这一章,我给出了一段配置,这段配置表明先依照 user_id % 2 对 t_order 进行水平拆分(到不同的子库),再依照 order_id % 2 对 t_order 进行水平拆分(到不同的子表)。但这并不是说使用了复合分片策略,而是使用了两个两个维度的标准分片策略。两个维度,分别是数据源分片策略(DatabaseShardingStrategy)和表分片策略(TableShardingStrategy),且在数据源分片策略上使用 user_id 作为单分片键、在表分片策略上使用 order_id 作为单分片键。

三、Hint(翻译为暗示) 分片策略

Hint 分片策略对应 HintShardingStrategy 这个 final class,同标准分片策略和符合分片策略的代码类似,HintShardingStrategy 中包含一个 HintShardingAlgorithm 接口的实例,并调用它的 doSharding方法。你们要自己去实现这个 HintShardingAlgorithm 接口中的 doSharding方法,这样我就能知道如何根据分片键的值,找到对应的分片表(或分片库)。此处不在展示 HintShardingStrategy 和 HintShardingAlgorithm 的源码。

Hint 分片策略是指通过 Hint 指定分片值而非从 SQL 中提取分片值的方式进行分片的策略。简单来讲就是我收到的 SQL 语句中不包含分片值(像上面给出的几段 SQL 就是包含分片值的 SQL),但是工程师会通过我提供的 Java API 将分片值暗示给我,这样我就知道怎样路由 SQL 查询到具体的分片了。就像下面这样:

String sql = "SELECT * FROM t_order";

try (

// HintManager 是使用“暗示”的工具,它会把暗示的分片值放入

// 当前线程上下文(ThreadLocal)中,这样当前线程执行 SQL 的

// 时候就能获取到分片值

HintManager hintManager = HintManager.getInstance;

Connection conn = dataSource.getConnection;

PreparedStatement pstmt = conn.prepareStatement(sql);

) {

hintManager.setDatabaseShardingValue(3);

try (ResultSet rs = pstmt.executeQuery) {

// 若 t_order 仅仅使用 order_id 作为分片键,则这里根据暗

// 示获取了分片值,因此上面的 SQL 的实际执行效果相当于:

// SELECT * FROM t_order where order_id = 3

while (rs.next) {

//...

}

}

}

四、不分片策略

对应 NoneShardingStrategy,这是一个 final class。由于我并不要求所有的表(或库)都进行水平分片,因此当工程师要通过我执行对不分片表(或库)的 SQL 查询时,就要使用这个不分片策略。NoneShardingStrategy 的源码为:

package org.apache.shardingsphere.core.strategy.route.none;

......

@Getter

public final classNoneShardingStrategyimplementsShardingStrategy{

private final Collection<String> shardingColumns = Collections.emptyList;

@Override

public Collection<String> doSharding(

// 所有可能的分片表(或分片库)名称

final Collection<String> availableTargetNames,

// 分片键的值

final Collection<RouteValue> shardingValues,

final ConfigurationProperties properties

) {

// 不需要任何算法,不进行任何逻辑处理,直接返回

// 所有可能的分片表(或分片库)名称

return availableTargetNames;

}

}

五、Inline 分片策略

Inline 分片策略,也叫做行表达式分片策略。Inline 分片策略对应 InlineShardingStrategy。Inline 分片策略是为用 Grovvy 表达式描述的分片算法准备的分片策略。文章开始展示的两段配置中就使用了 Inline 分片策略。InlineShardingStrategy 把 Grovvy 表达式当做分片算法的实现,因此 HintShardingStrategy 中不包含算法域变量,这一点有别于 StandardShardingStrategy 等 final class。这里不再展示 InlineShardingStrategy 的源码。

我知道,这段关于分片策略和分片算法的表述很难理解。不过我还是想让你们明白,无论对某个逻辑表(或库)进行怎样的分片策略配置,这些策略不过都是在告诉我怎样处理分片,也就是告诉我如何根据分片键的值,找到对应的分片表(或分片库)。只不过我的创造者把这个简单的过程翻出了很多花样,也就是你们在上面看到的各种策略,以提供使用上的灵活性。

4.2.5. 绑定表

指分片规则一致的主表和子表。例如 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_order0 o JOIN t_order_item0 i ON o.order_id=i.order_id WHERE o.order_id IN (10, 11);

SELECT i.* FROM t_order0 o JOIN t_order_item1 i ON o.order_id=i.order_id WHERE o.order_id IN (10, 11);

SELECT i.* FROM t_order1 o JOIN t_order_item0 i ON o.order_id=i.order_id WHERE o.order_id IN (10, 11);

SELECT i.* FROM t_order1 o JOIN t_order_item1 i ON o.order_id=i.order_id WHERE o.order_id IN (10, 11);

而在配置绑定表关系后,路由的 SQL 只有 2 条:

SELECT i.* FROM t_order0 o JOIN t_order_item0 i ON o.order_id=i.order_id WHERE o.order_id IN (10, 11);

SELECT i.* FROM t_order1 o JOIN t_order_item1 i ON o.order_id=i.order_id WHERE o.order_id IN (10, 11);

而我也提供了这种绑定关系配置的 API 和配置项,例如在 properties 配置文件中可以这么写:

# 设置绑定表

sharding.jdbc.config.sharding.binding-tables=t_order, t_order_item

4.3. 我处理 SQL 的过程

我是 Sharding-JDBC,我是水平分片世界的神。我的职责是透明化水平分库分表所带来的影响,让使用方尽量像使用一个数据库一样使用水平分片之后的数据库集群,或者像使用一个数据表一样使用水平分片之后的数据表。

我的法力,来源于我的创造者为我设计的内核,它把 SQL 语句的处理分成了 SQL 解析 =>SQL 路由 => SQL 改写 => SQL 执行 => 结果归并五个主要流程。

039

当一个应用服务器节点将一个面向逻辑表编写的查询 SQL 交给我之后,我要做下面几件事:

1)SQL 解析(由我内核中的解析引擎完成):先通过词法解析器将逻辑 SQL 拆分为一个个不可再分的单词,再使用语法解析器对 SQL 进行理解,并最终提炼出解析上下文。

2)SQL 路由(由我内核中的路由引擎完成):根据解析上下文匹配用户配置的分片策略(关于用户配置的分片策略,我后文会慢慢解释),并生成路由路径,路由路径指示了 SQL 最终要到哪些分片去执行。

3)SQL 改写(由我内核中的改写引擎完成):将 面向逻辑表 SQL 改写为在真实数据库中可以正确执行的语句(逻辑 SQL 到物理 SQL 的映射)。

4)SQL 执行(由我内核中的执行引擎完成):通过多线程执行器异步执行路由和改写之后得到的 SQL 语句。

5)结果归并(由我内核中的归并引擎完成):将多个执行结果集归并以便于通过统一的 JDBC 接口输出。

4.3.1. SQL 解析

SQL 解析 SQL 解析分为词法解析和语法解析。

我的解析引擎先通过词法解析器将这句 SQL 拆分为一个个不可再分的单词,再使用语法解析器对 SQL 进行理解,并最终提炼出解析上下文。解析上下文包括表、选择项、排序项、分组项、聚合函数、分页信息、查询条件以及可能需要修改的占位符的标记。简单来说就是我要理解这句 SQL,明白它的结构和意图。所幸,SQL 是一个语法简单的语言,SQL 解析这件事情并不复杂。

我先使用解析引擎的词法解析器用于将 SQL 拆解为不可再分的原子符号,我把它们叫做 Token,并将其归类为关键字、表达式、字面量、操作符,再使用解析引擎的语法解析器将 SQL 转换为抽象语法树。

例如,以下 SQL:

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

被我的词法解析器和语法解析器解析之后得到的抽象语法树为:

在上图中,为了便于理解,抽象语法树中的关键字和操作符的 Token 用绿⾊表示,字面量的 Token 用红⾊表示,灰⾊表示需要进一步拆分。

最后,我通过对抽象语法树的遍历去提炼分片所需的上下文,并标记有可能需要改写的位置。供分片使用的解析上下文包含查询选择项(Select Items)、表信息(Table)、分片条件(Sharding Condition)、自增主键信息(Auto increment Primary Key)、排序信息(Order By)、分组信息(Group By)以及分页信息(Limit、Rownum、Top)。

SQL 解析是下面的

标签: #039oraclebi解决方案 #mysql中的占位符 #mysql56jdbc