first commit

This commit is contained in:
张乾
2024-10-16 13:06:13 +08:00
parent 2393162ba9
commit c47809d1ff
41 changed files with 9189 additions and 0 deletions

View File

@ -0,0 +1,256 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
06 数据分片:如何实现分库、分表、分库+分表以及强制路由?(上)
通过前面几个课时的介绍,相信你对 ShardingSphere 已经有了初步了解。从今天开始,我将带领你通过案例分析逐步掌握 ShardingSphere 的各项核心功能,首当其冲的就是分库分表机制。
单库单表系统
我们先从单库单表系统说起。在整个课程中,如果没有特殊强调,我们将默认使用 Spring Boot 集成和 ShardingSphere 框架,同时基于 Mybatis 实现对数据库的访问。
导入开发框架
系统开发的第一步是导入所需的开发框架。在下面这段代码中,我们新建了一个 Spring Boot 代码工程,在 pom 文件中需要添加对 sharding-jdbc-spring-boot-starter 和 mybatis-spring-boot-starter 这两个 starter 的引用:
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
开发环境初始化要做的工作也就是这些,下面我们来介绍案例的业务场景。
梳理业务场景
我们考虑一个在医疗健康领域中比较常见的业务场景。在这类场景中每个用户User都有一份健康记录HealthRecord存储着代表用户当前健康状况的健康等级HealthLevel以及一系列健康任务HealthTask。通常医生通过用户当前的健康记录创建不同的健康任务然后用户可以通过完成医生所指定的任务来获取一定的健康积分而这个积分决定了用户的健康等级并最终影响到整个健康记录。健康任务做得越多健康等级就越高用户的健康记录也就越完善反过来健康任务也就可以越做越少从而形成一个正向的业务闭环。这里我们无意对整个业务闭环做过多的阐述而是关注这一业务场景下几个核心业务对象的存储和访问方式。
在这个场景下,我们关注 User、HealthRecord、HealthLevel 和 HealthTask 这四个业务对象。在下面这张图中,对每个业务对象给出最基础的字段定义,以及这四个对象之间的关联关系:
完成基础功能
既然采用 Mybatis 作为 ORM 框架,那么就需要遵循 Mybatis 的开发流程。首先,我们需要完成各个业务实体的定义:
业务实体的类定义
基于这些业务实体,我们需要完成对应的 Mapper 文件编写,我把这些 Mapper 文件放在代码工程的 resources 目录下:
Mybatis Mapper 文件定义
下一步是数据源信息的配置,我们把这些信息放在一个单独的 application-traditional.properties 配置文件中。
spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.url = jdbc:mysql://localhost:3306/ds
spring.datasource.username = root
spring.datasource.password = root
按照 Spring Boot 的配置约定,我们在 application.properties 配置文件中把上述配置文件设置为启动 profile。通过使用不同的 profile我们可以完成不同配置体系之间的切换。
spring.profiles.active=traditional
接下来要做的事情就是创建 Repository 层组件:
Repository 层接口定义
最后,我们设计并实现了相关的三个服务类,分别是 UserService、HealthLevelService 和 HealthRecordService。
Service 层接口和实现类定义
通过 UserService我们会插入一批用户数据用于完成用户信息的初始化。然后我们有一个 HealthLevelService专门用来初始化健康等级信息。请注意与其他业务对象不同健康等级信息是系统中的一种典型字典信息我们假定系统中存在 5 种健康等级。
第三个,也是最重要的服务就是 HealthRecordService我们用它来完成 HealthRecord 以及 HealthTask 数据的存储和访问。这里以 HealthRecordService 服务为例,下面这段代码给出了它的实现过程:
@Service
public class HealthRecordServiceImpl implements HealthRecordService {
@Autowired
private HealthRecordRepository healthRecordRepository;
@Autowired
private HealthTaskRepository healthTaskRepository;
@Override
public void processHealthRecords() throws SQLException{
insertHealthRecords();
}
private List<Integer> insertHealthRecords() throws SQLException {
List<Integer> result = new ArrayList<>(10);
for (int i = 1; i <= 10; i++) {
HealthRecord healthRecord = insertHealthRecord(i);
insertHealthTask(i, healthRecord);
result.add(healthRecord.getRecordId());
}
return result;
}
private HealthRecord insertHealthRecord(final int i) throws SQLException {
HealthRecord healthRecord = new HealthRecord();
healthRecord.setUserId(i);
healthRecord.setLevelId(i % 5);
healthRecord.setRemark("Remark" + i);
healthRecordRepository.addEntity(healthRecord);
return healthRecord;
}
private void insertHealthTask(final int i, final HealthRecord healthRecord) throws SQLException {
HealthTask healthTask = new HealthTask();
healthTask.setRecordId(healthRecord.getRecordId());
healthTask.setUserId(i);
healthTask.setTaskName("TaskName" + i);
healthTaskRepository.addEntity(healthTask);
}
}
现在,我们已经从零开始实现了一个完整业务场景所需要的 DAO 层和 Service 层组件。这些组件在业务逻辑上都非常简单,而在技术上也是完全采用了 Mybatis 的经典开发过程。最后,我们可以通过一组简单的单元测试来验证这些组件是否能够正常运行。下面这段代码以 UserServiceTest 类为例给出它的实现,涉及 @RunWith@SpringBootTest 等常见单元测试注解的使用:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testProcessUsers() throws Exception {
userService.processUsers();
}
}
运行这个单元测试,我们可以看到测试通过,并且在数据库的 User 表中也看到了插入的数据。至此,一个单库单表的系统已经构建完成。接下来,我们将对这个系统做分库分表改造。
在传统单库单表的数据架构上进行分库分表的改造,开发人员只需要做一件事情,那就是基于上一课时介绍的 ShardingSphere 配置体系完成针对具体场景的配置工作即可,所有已经存在的业务代码都不需要做任何的变动,这就是 ShardingSphere 的强大之处。让我们一起开始吧。
系统改造:如何实现分库?
作为系统改造的第一步,我们首先来看看如何基于配置体系实现数据的分库访问。
初始化数据源
针对分库场景,我们设计了两个数据库,分别叫 ds0 和 ds1。显然针对两个数据源我们就需要初始化两个 DataSource 对象,这两个 DataSource 对象将组成一个 Map 并传递给 ShardingDataSourceFactory 工厂类:
spring.shardingsphere.datasource.names=ds0,ds1
spring.shardingsphere.datasource.ds0.type=com.alibaba.druid.pool.DruidDataSource
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=root
spring.shardingsphere.datasource.ds1.type=com.alibaba.druid.pool.DruidDataSource
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=root
设置分片策略
明确了数据源之后,我们需要设置针对分库的分片策略:
spring.shardingsphere.sharding.default-database-strategy.inline.sharding-column=user_id
spring.shardingsphere.sharding.default-database-strategy.inline.algorithm-expression=ds$->{user_id % 2}
我们知道,在 ShardingSphere 中存在一组 ShardingStrategyConfiguration这里使用的是基于行表达式的 InlineShardingStrategyConfiguration。
InlineShardingStrategyConfiguration 包含两个需要设置的参数,一个是指定分片列名称的 shardingColumn另一个是指定分片算法行表达式的 algorithmExpression。在我们的配置方案中将基于 user_id 列对 2 的取模值来确定数据应该存储在哪一个数据库中。同时注意到这里配置的是“default-database-strategy”项。结合上一课时的内容设置这个配置项相当于是在 ShardingRuleConfiguration 中指定了默认的分库 ShardingStrategy。
设置绑定表和广播表
接下来我们需要设置绑定表。绑定表BindingTable是 ShardingSphere 中提出的一个新概念,我来给你解释一下。
所谓绑定表是指与分片规则一致的一组主表和子表。例如在我们的业务场景中health_record 表和 health_task 表中都存在一个 record_id 字段。如果我们在应用过程中按照这个 record_id 字段进行分片,那么这两张表就可以构成互为绑定表关系。
引入绑定表概念的根本原因在于,互为绑定表关系的多表关联查询不会出现笛卡尔积,因此关联查询效率将大大提升。举例说明,如果所执行的为下面这条 SQL
SELECT record.remark_name FROM health_record record JOIN health_task task ON record.record_id=task.record_id WHERE record.record_id in (1, 2);
如果我们不显式配置绑定表关系,假设分片键 record_id 将值 1 路由至第 1 片,将数值 2 路由至第 0 片,那么路由后的 SQL 应该为 4 条,它们呈现为笛卡尔积:
SELECT record.remark_name FROM health_record0 record JOIN health_task0 task ON record.record_id=task.record_id WHERE record.record_id in (1, 2);
SELECT record.remark_name FROM health_record0 record JOIN health_task1 task ON record.record_id=task.record_id WHERE record.record_id in (1, 2);
SELECT record.remark_name FROM health_record1 record JOIN health_task0 task ON record.record_id=task.record_id WHERE record.record_id in (1, 2);
SELECT record.remark_name FROM health_record1 record JOIN health_task1 task ON record.record_id=task.record_id WHERE record.record_id in (1, 2);
然后,在配置绑定表关系后,路由的 SQL 就会减少到 2 条:
SELECT record.remark_name FROM health_record0 record JOIN health_task0 task ON record.record_id=task.record_id WHERE record.record_id in (1, 2);
SELECT record.remark_name FROM health_record1 record JOIN health_task1 task ON record.record_id=task.record_id WHERE record.record_id in (1, 2);
请注意,如果想要达到这种效果,互为绑定表的各个表的分片键要完全相同。在上面的这些 SQL 语句中,我们不难看出,这个需要完全相同的分片键就是 record_id。
让我们回到案例中的场景显然health_record 和 health_task 应该互为绑定表关系。所以,我们可以在配置文件中添加对这种关系的配置:
spring.shardingsphere.sharding.binding-tables=health_record, health_task
介绍完绑定表再来看广播表的概念。所谓广播表BroadCastTable是指所有分片数据源中都存在的表也就是说这种表的表结构和表中的数据在每个数据库中都是完全一样的。广播表的适用场景比较明确通常针对数据量不大且需要与海量数据表进行关联查询的应用场景典型的例子就是每个分片数据库中都应该存在的字典表。
同样回到我们的场景,对于 health_level 表而言,由于它保存着有限的健康等级信息,可以认为它就是这样的一种字典表。所以,我们也在配置文件中添加了对广播表的定义,在下面这段代码中你可以看到:
spring.shardingsphere.sharding.broadcast-tables=health_level
设置表分片规则
通过前面的这些配置项,我们根据需求完成了 ShardingRuleConfiguration 中与分库操作相关的配置信息设置。我们知道 ShardingRuleConfiguration 中的 TableRuleConfiguration 是必填项。所以,我们来看一下这个场景下应该如何对表分片进行设置。
TableRuleConfiguration 是表分片规则配置,包含了用于设置真实数据节点的 actualDataNodes用于设置分库策略的 databaseShardingStrategyConfig以及用于设置分布式环境下的自增列生成器的 keyGeneratorConfig。前面已经在 ShardingRuleConfiguration 中设置了默认的 databaseShardingStrategyConfig现在我们需要完成剩下的 actualDataNodes 和 keyGeneratorConfig 的设置。
对于 health_record 表而言,由于存在两个数据源,所以,它所属于的 actual-data-nodes 可以用行表达式 ds$->{0..1}.health_record 来进行表示,代表在 ds0 和 ds1 中都存在表 health_record。而对于 keyGeneratorConfig 而言通常建议你使用雪花算法。明确了这些信息之后health_record 表对应的 TableRuleConfiguration 配置也就顺理成章了:
spring.shardingsphere.sharding.tables.health_record.actual-data-nodes=ds$->{0..1}.health_record
spring.shardingsphere.sharding.tables.health_record.key-generator.column=record_id
spring.shardingsphere.sharding.tables.health_record.key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.tables.health_record.key-generator.props.worker.id=33
同样的health_task 表的配置也完全类似,这里需要根据实际情况调整 key-generator.column 的具体数据列:
spring.shardingsphere.sharding.tables.health_task.actual-data-nodes=ds$->{0..1}.health_task
spring.shardingsphere.sharding.tables.health_task.key-generator.column=task_id
spring.shardingsphere.sharding.tables.health_task.key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.tables.health_task.key-generator.props.worker.id=33
让我们重新执行 HealthRecordTest 单元测试,并检查数据库中的数据。下面这张图是 ds0 中的 health_record 和 health_task 表:
ds0 中 health_record 表数据
ds0 中 health_task 表数据
而这张图是 ds1 中的 health_record 和 health_task 表:
ds1 中 health_record 表数据
ds1 中 health_task 表数据
显然,这两张表的数据已经正确进行了分库。
小结
从本课时开始,我们正式进入到 ShardingSphere 核心功能的讲解。为了介绍这些功能特性,我们将从单库单表架构讲起,基于一个典型的业务场景梳理数据操作的需求,并给出整个代码工程的框架,以及基于测试用例验证数据操作结果的实现过程。今天的内容关注于如何实现分库操作,我们通过引入 ShardingSphere 中强大的配置体系实现了分库效果。
这里给你留一道思考题:如何理解绑定表和广播表的含义和作用?
分库是 ShardingSphere 中分片引擎的核心功能之一,也可以说是最简单的功能之一。在下一课时中,我们将继续介绍分表、分库+分表以及强制路由等分片机制。

View File

@ -0,0 +1,380 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
07 数据分片:如何实现分库、分表、分库+分表以及强制路由?(下)
在上一课时中,我们基于业务场景介绍了如何将单库单表架构改造成分库架构。今天我们继续后续的改造工作,主要涉及如何实现分表、分库+分表以及如何实现强制路由。
系统改造:如何实现分表?
相比分库,分表操作是在同一个数据库中,完成对一张表的拆分工作。所以从数据源上讲,我们只需要定义一个 DataSource 对象即可,这里把这个新的 DataSource 命名为 ds2
spring.shardingsphere.datasource.names=ds2
spring.shardingsphere.datasource.ds2.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds2.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds2.url=jdbc:mysql://localhost:3306/ds2
spring.shardingsphere.datasource.ds2.username=root
spring.shardingsphere.datasource.ds2.password=root
同样,为了提高访问性能,我们设置了绑定表和广播表:
spring.shardingsphere.sharding.binding-tables=health_record, health_task
spring.shardingsphere.sharding.broadcast-tables=health_level
现在,让我们再次回想起 TableRuleConfiguration 配置,该配置中的 tableShardingStrategyConfig 代表分表策略。与用于分库策略的 databaseShardingStrategyConfig 一样,设置分表策略的方式也是指定一个用于分表的分片键以及分片表达式:
spring.shardingsphere.sharding.tables.health_record.table-strategy.inline.sharding-column=record_id
spring.shardingsphere.sharding.tables.health_record.table-strategy.inline.algorithm-expression=health_record$->{record_id % 2}
在代码中可以看到,对于 health_record 表而言,我们设置它用于分表的分片键为 record_id以及它的分片行表达式为 health_record$->{record_id % 2}。也就是说,我们会根据 record_id 将 health_record 单表拆分成 health_record0 和 health_record1 这两张分表。
基于分表策略,再加上 actualDataNodes 和 keyGeneratorConfig 配置项,我们就可以完成对 health_record 表的完整分表配置:
spring.shardingsphere.sharding.tables.health_record.actual-data-nodes=ds2.health_record$->{0..1}
spring.shardingsphere.sharding.tables.health_record.table-strategy.inline.sharding-column=record_id
spring.shardingsphere.sharding.tables.health_record.table-strategy.inline.algorithm-expression=health_record$->{record_id % 2}
spring.shardingsphere.sharding.tables.health_record.key-generator.column=record_id
spring.shardingsphere.sharding.tables.health_record.key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.tables.health_record.key-generator.props.worker.id=33
对于 health_task 表而言,可以采用同样的配置方法完成分表操作:
spring.shardingsphere.sharding.tables.health_task.actual-data-nodes=ds2.health_task$->{0..1}
spring.shardingsphere.sharding.tables.health_task.table-strategy.inline.sharding-column=record_id
spring.shardingsphere.sharding.tables.health_task.table-strategy.inline.algorithm-expression=health_task$->{record_id % 2}
spring.shardingsphere.sharding.tables.health_task.key-generator.column=task_id
spring.shardingsphere.sharding.tables.health_task.key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.tables.health_task.key-generator.props.worker.id=33
可以看到,由于 health_task 与 health_record 互为绑定表,所以在 health_task 的配置中,我们同样基于 record_id 列进行分片,也就是说,我们会根据 record_id 将 health_task 单表拆分成 health_task0 和 health_task1 两张分表。当然,自增键的生成列还是需要设置成 health_task 表中的 task_id 字段。
这样,完整的分表配置就完成了。现在,让我们重新执行 HealthRecordTest 单元测试,会发现数据已经进行了正确的分表。下图是分表之后的 health_record0 和 health_record1 表:
分表后的 health_record0 表数据
分表后的 health_record1 表数据
而这是分表之后的 health_task0 和 health_task1 表:
分表后的 health_task0 表数据
分表后的 health_task1表数据
系统改造:如何实现分库+分表?
在完成独立的分库和分表操作之后,系统改造的第三步是尝试把分库和分表结合起来。这个过程听起来比较复杂,但事实上,基于 ShardingSphere 提供的强大配置体系,开发人员要做的只是将分表针对分库和分表的配置项整合在一起就可以了。这里我们重新创建 3 个新的数据源,分别为 ds3、ds4 和 ds5:
spring.shardingsphere.datasource.names=ds3,ds4,ds5
spring.shardingsphere.datasource.ds3.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds3.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds3.url=jdbc:mysql://localhost:3306/ds3
spring.shardingsphere.datasource.ds3.username=root
spring.shardingsphere.datasource.ds3.password=root
spring.shardingsphere.datasource.ds4.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds4.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds4.url=jdbc:mysql://localhost:3306/ds4
spring.shardingsphere.datasource.ds4.username=root
spring.shardingsphere.datasource.ds4.password=root
spring.shardingsphere.datasource.ds5.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds5.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds5.url=jdbc:mysql://localhost:3306/ds5
spring.shardingsphere.datasource.ds5.username=root
spring.shardingsphere.datasource.ds5.password=root
注意,到现在有 3 个数据源,而且命名分别是 ds3、ds4 和 ds5。所以为了根据 user_id 来将数据分别分片到对应的数据源,我们需要调整行表达式,这时候的行表达式应该是 ds$->{user_id % 3 + 3}
spring.shardingsphere.sharding.default-database-strategy.inline.sharding-column=user_id
spring.shardingsphere.sharding.default-database-strategy.inline.algorithm-expression=ds$->{user_id % 3 + 3}
spring.shardingsphere.sharding.binding-tables=health_record,health_task
spring.shardingsphere.sharding.broadcast-tables=health_level
对于 health_record 和 health_task 表而言,同样需要调整对应的行表达式,我们将 actual-data-nodes 设置为 ds\(->{3..5}.health_record\)->{0..2},也就是说每张原始表将被拆分成 3 张分表:
spring.shardingsphere.sharding.tables.health_record.actual-data-nodes=ds$->{3..5}.health_record$->{0..2}
spring.shardingsphere.sharding.tables.health_record.table-strategy.inline.sharding-column=record_id
spring.shardingsphere.sharding.tables.health_record.table-strategy.inline.algorithm-expression=health_record$->{record_id % 3}
spring.shardingsphere.sharding.tables.health_record.key-generator.column=record_id
spring.shardingsphere.sharding.tables.health_record.key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.tables.health_record.key-generator.props.worker.id=33
spring.shardingsphere.sharding.tables.health_task.actual-data-nodes=ds$->{3..5}.health_task$->{0..2}
spring.shardingsphere.sharding.tables.health_task.table-strategy.inline.sharding-column=record_id
spring.shardingsphere.sharding.tables.health_task.table-strategy.inline.algorithm-expression=health_task$->{record_id % 3}
spring.shardingsphere.sharding.tables.health_task.key-generator.column=task_id
spring.shardingsphere.sharding.tables.health_task.key-generator.type=SNOWFLAKE
spring.shardingsphere.sharding.tables.health_task.key-generator.props.worker.id=33
这样,整合分库+分表的配置方案就介绍完毕了,可以看到,这里并没有引入任何新的配置项让我们重新执行单元测试,从而确认数据是否已经正确地进行了分库分表。这是 ds3 中的 health_record0、health_record1 和 health_record2 表:
ds3 中的 health_record0 表数据
ds3 中的 health_record1 表数据
ds3 中的 health_record2 表数据
这是 ds4 中的 health_record0、health_record1 和 health_record2 表:
ds4 中的 health_record0 表数据
ds4 中的 health_record1 表数据
ds4 中的 health_record2 表数据
而下面是 ds5 中的 health_record0、health_record1 和 health_record2 表:
ds5 中的 health_record0 表数据
ds5 中的 health_record1 表数据
ds5 中的 health_record2 表数据
对于 health_task 表而言,我们得到的也是类似的分库分表效果。
系统改造:如何实现强制路由?
从 SQL 执行效果而言,分库分表可以看作是一种路由机制,也就是说把 SQL 语句路由到目标数据库或数据表中并获取数据。在实现了分库分表的基础之上,我们将要引入一种不同的路由方法,即强制路由。
什么是强制路由?
强制路由与一般的分库分表路由不同,它并没有使用任何的分片键和分片策略。我们知道通过解析 SQL 语句提取分片键,并设置分片策略进行分片是 ShardingSphere 对重写 JDBC 规范的实现方式。但是,如果我们没有分片键,是否就只能访问所有的数据库和数据表进行全路由呢?显然,这种处理方式也不大合适。有时候,我们需要为 SQL 执行开一个“后门”,允许在没有分片键的情况下,同样可以在外部设置目标数据库和表,这就是强制路由的设计理念。
在 ShardingSphere 中,通过 Hint 机制实现强制路由。我们在这里对 Hint 这一概念再做进一步的阐述。在关系型数据库中Hint 作为一种 SQL 补充语法扮演着非常重要的角色。它允许用户通过相关的语法影响 SQL 的执行方式,改变 SQL 的执行计划,从而对 SQL 进行特殊的优化。很多数据库工具也提供了特殊的 Hint 语法。以 MySQL 为例,比较典型的 Hint 使用方式之一就是对所有索引的强制执行和忽略机制。
MySQL 中的强制索引能够确保所需要执行的 SQL 语句只作用于所指定的索引上,我们可以通过 FORCE INDEX 这一 Hint 语法实现这一目标:
SELECT * FROM TABLE1 FORCE INDEX (FIELD1)
类似的IGNORE INDEX 这一 Hint 语法使得原本设置在具体字段上的索引不被使用:
SELECT * FROM TABLE1 IGNORE INDEX (FIELD1, FIELD2)
对于分片字段非 SQL 决定、而由其他外置条件决定的场景,可使用 SQL Hint 灵活地注入分片字段。
如何设计和开发强制路由?
基于 Hint 进行强制路由的设计和开发过程需要遵循一定的约定同时ShardingSphere 也提供了专门的 HintManager 来简化强制路由的开发过程。
HintManager
HintManager 类的使用方式比较固化,我们可以通过查看源码中的类定义以及核心变量来理解它所包含的操作内容:
public final class HintManager implements AutoCloseable {
//基于ThreadLocal存储HintManager实例
private static final ThreadLocal<HintManager> HINT_MANAGER_HOLDER = new ThreadLocal<>();
//数据库分片值
private final Multimap<String, Comparable<?>> databaseShardingValues = HashMultimap.create();
//数据表分片值
private final Multimap<String, Comparable<?>> tableShardingValues = HashMultimap.create();
//是否只有数据库分片
private boolean databaseShardingOnly;
//是否只路由主库
private boolean masterRouteOnly;
}
在变量定义上,我们注意到 HintManager 使用了 ThreadLocal 来保存 HintManager 实例。显然,基于这种处理方式,所有分片信息的作用范围就是当前线程。我们也看到了用于分别存储数据库分片值和数据表分片值的两个 Multimap 对象以及分别用于指定是否只有数据库分片以及是否只路由主库的标志位。可以想象HintManager 基于这些变量开放了一组 get/set 方法供开发人员根据具体业务场景进行分片键的设置。
同时,在类的定义上,我们也注意到 HintManager 实现了 AutoCloseable 接口,这个接口是在 JDK7 中引入的一个新接口用于自动释放资源。AutoCloseable 接口只有一个 close 方法,我们可以实现这个方法来释放自定义的各种资源。
public interface AutoCloseable {
void close() throws Exception;
}
在 JDK1.7 之前,我们需要手动通过 try/catch/finally 中的 finally 语句来释放资源,而使用 AutoCloseable 接口,在 try 语句结束的时候,不需要实现 finally 语句就会自动将这些资源关闭JDK 会通过回调的方式,调用 close 方法来做到这一点。这种机制被称为 try with resource。AutoCloseable 还提供了语法糖,在 try 语句中可以同时使用多个实现这个接口的资源,并通过使用分号进行分隔。
HintManager 中通过实现 AutoCloseable 接口支持资源的自动释放事实上JDBC 中的 Connection 和 Statement 接口的实现类同样也实现了这个 AutoCloseable 接口。
对于 HintManager 而言,所谓的资源实际上就是 ThreadLocal 中所保存的 HintManager 实例。下面这段代码实现了 AutoCloseable 接口的 close 方法,进行资源的释放:
public static void clear() {
HINT_MANAGER_HOLDER.remove();
}
@Override
public void close() {
HintManager.clear();
}
HintManager 的创建过程使用了典型的单例设计模式,下面这段代码展现了通过一个静态的 getInstance 方法,从 ThreadLocal 中获取或设置针对当前线程的 HintManager 实例。
public static HintManager getInstance() {
Preconditions.checkState(null == HINT_MANAGER_HOLDER.get(), "Hint has previous value, please clear first.");
HintManager result = new HintManager();
HINT_MANAGER_HOLDER.set(result);
return result;
}
在理解了 HintManager 的基本结构之后,在应用程序中获取 HintManager 的过程就显得非常简单了,这里给出推荐的使用方式:
try (HintManager hintManager = HintManager.getInstance();
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement()) {
}
可以看到,我们在 try 语句中获取了 HintManager、Connection 和 Statement 实例,然后就可以基于这些实例来完成具体的 SQL 执行。
实现并配置强制路由分片算法
开发基于 Hint 的强制路由的基础还是配置。在介绍与 Hint 相关的配置项之前,让我们回想在 05 课时“ShardingSphere 中的配置体系是如何设计的?”中介绍的 TableRuleConfiguration。我们知道 TableRuleConfiguration 中包含两个 ShardingStrategyConfiguration分别用于设置分库策略和分表策略。而 ShardingSphere 专门提供了 HintShardingStrategyConfiguration 用于完成 Hint 的分片策略配置,如下面这段代码所示:
public final class HintShardingStrategyConfiguration implements ShardingStrategyConfiguration {
private final HintShardingAlgorithm shardingAlgorithm;
public HintShardingStrategyConfiguration(final HintShardingAlgorithm shardingAlgorithm) {
Preconditions.checkNotNull(shardingAlgorithm, "ShardingAlgorithm is required.");
this.shardingAlgorithm = shardingAlgorithm;
}
}
可以看到HintShardingStrategyConfiguration 中需要设置一个 HintShardingAlgorithm。HintShardingAlgorithm 是一个接口,我们需要提供它的实现类来根据 Hint 信息执行分片。
public interface HintShardingAlgorithm<T extends Comparable<?>> extends ShardingAlgorithm {
//根据Hint信息执行分片
Collection<String> doSharding(Collection<String> availableTargetNames, HintShardingValue<T> shardingValue);
}
在 ShardingSphere 中内置了一个 HintShardingAlgorithm 的实现类 DefaultHintShardingAlgorithm但这个实现类并没有执行任何的分片逻辑只是将传入的所有 availableTargetNames 直接进行返回而已,如下面这段代码所示:
public final class DefaultHintShardingAlgorithm implements HintShardingAlgorithm<Integer> {
@Override
public Collection<String> doSharding(final Collection<String> availableTargetNames, final HintShardingValue<Integer> shardingValue) {
return availableTargetNames;
}
}
我们可以根据需要提供自己的 HintShardingAlgorithm 实现类并集成到 HintShardingStrategyConfiguration 中。例如,我们可以对比所有可用的分库分表键值,然后与传入的强制分片键进行精准匹配,从而确定目标的库表信息:
public final class MatchHintShardingAlgorithm implements HintShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(final Collection<String> availableTargetNames, final HintShardingValue<Long> shardingValue) {
Collection<String> result = new ArrayList<>();
for (String each : availableTargetNames) {
for (Long value : shardingValue.getValues()) {
if (each.endsWith(String.valueOf(value))) {
result.add(each);
}
}
}
return result;
}
}
一旦提供了自定的 HintShardingAlgorithm 实现类,就需要将它添加到配置体系中。在这里,我们基于 Yaml 配置风格来完成这一操作:
defaultDatabaseStrategy:
hint:
algorithmClassName: com.tianyilan.shardingsphere.demo.hint.MatchHintShardingAlgorithm
ShardingSphere 在进行路由时,如果发现 TableRuleConfiguration 中设置了 Hint 的分片算法,就会从 HintManager 中获取分片值并进行路由操作。
如何基于强制路由访问目标库表?
在理解了强制路由的概念和开发过程之后,让我们回到案例。这里以针对数据库的强制路由为例,给出具体的实现过程。为了更好地组织代码结构,我们先来构建两个 Helper 类,一个是用于获取 DataSource 的 DataSourceHelper。在这个 Helper 类中,我们通过加载 .yaml 配置文件来完成 DataSource 的构建:
public class DataSourceHelper {
static DataSource getDataSourceForShardingDatabases() throws IOException, SQLException {
return YamlShardingDataSourceFactory.createDataSource(getFile("/META-INF/hint-databases.yaml"));
}
private static File getFile(final String configFile) {
return new File(Thread.currentThread().getClass().getResource(configFile).getFile());
}
}
这里用到了 YamlShardingDataSourceFactory 工厂类,针对 Yaml 配置的实现方案你可以回顾 05 课时中的内容。
另一个 Helper 类是包装 HintManager 的 HintManagerHelper。在这个帮助类中我们通过使用 HintManager 开放的 setDatabaseShardingValue 来完成数据库分片值的设置。在这个示例中我们只想从第一个库中获取目标数据。HintManager 还提供了 addDatabaseShardingValue 和 addTableShardingValue 等方法设置强制路由的分片值。
public class HintManagerHelper {
static void initializeHintManagerForShardingDatabases(final HintManager hintManager) {
hintManager.setDatabaseShardingValue(1L);
}
}
最后,我们构建一个 HintService 来完成整个强制路由流程的封装:
public class HintService {
private static void processWithHintValueForShardingDatabases() throws SQLException, IOException {
DataSource dataSource = DataSourceHelper.getDataSourceForShardingDatabases();
try (HintManager hintManager = HintManager.getInstance();
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement()) {
HintManagerHelper.initializeHintManagerForShardingDatabases(hintManager);
ResultSet result = statement.executeQuery("select * from health_record");
while (result.next()) {
System.out.println(result.getInt(0) + result.getString(1));
}
}
}
}
可以看到,在这个 processWithHintValueForShardingDatabases 方法中,我们首先通过 DataSourceHelper 获取目标 DataSource。然后使用 try with resource 机制在 try 语句中获取了 HintManager、Connection 和 Statement 实例,并通过 HintManagerHelper 帮助类设置强制路由的分片值。最后,通过 Statement 来执行一个全表查询,并打印查询结果:
2020-05-25 21:58:13.932 INFO 20024 --- [ main] ShardingSphere-SQL : Logic SQL: select user_id, user_name from user
2020-05-25 21:58:13.932 INFO 20024 --- [ main] ShardingSphere-SQL : Actual SQL: ds1 ::: select user_id, user_name from user
6: user_6
7: user_7
8: user_8
9: user_9
10: user_10
我们获取执行过程中的日志信息,可以看到原始的逻辑 SQL 是 select user_id, user_name from user而真正执行的真实 SQL 则是 ds1 ::: select user_id, user_name from user。显然强制路由发生了效果我们获取的只是 ds1 中的所有 User 信息。
小结
承接上一课时的内容,今天我们继续在对单库单表架构进行分库操作的基础上,讲解如何实现分表、分库+分表以及强制路由的具体细节。有了分库的实践经验,要完成分表以及分库分表是比较容易的,所做的工作只是调整和设置对应的配置项。而强制路由是一种新的路由机制,我们通过较大的篇幅来对它的概念和实现方法进行了展开,并结合业务场景给出了案例分析。
这里给你留一道思考题ShardingSphere 如何基于 Hint 机制实现分库分表场景下的强制路由?
从路由的角度讲,基于数据库主从架构的读写分离机制也可以被认为是一种路由。在下一课时的内容中,我们将对 ShardingSphere 提供的读写分离机制进行讲解,并同样给出读写分离与分库分表、强制路由进行整合的具体方法。

View File

@ -0,0 +1,189 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
08 读写分离:如何集成分库分表+数据库主从架构?
为了应对高并发场景下的数据库访问需求,读写分离架构是现代数据库架构的一个重要组成部分。今天,我就和你一起来学习 ShardingSphere 中所提供的读写分离机制,以及这一机制如何与前面介绍的分库分表和强制路由整合在一起使用。
ShardingSphere 中的读写分离
为了应对数据库读写分离ShardingSphere 所提供的解决方案还是依赖于强大的配置体系。为了更好地理解这些读写分离相关的配置,我们有必要对读写分离与主从架构有一定的了解。
读写分离与主从架构
目前,大部分的主流关系型数据库都提供了主从架构的实现方案,通过配置两台或多台数据库的主从关系,可以将一台数据库服务器的数据更新自动同步到另一台服务器上。而应用程序可以利用数据库的这一功能,实现数据的读写分离,从而改善数据库的负载压力。
可以看到,所谓的读写分离,实际上就是将写操作路由到主数据库,而将读操作路由到从数据库。对于互联网应用而言,读取数据的需求远远大于写入数据的需求,所以从数据库一般都是多台。当然,对于复杂度较高的系统架构而言,主库的数量同样也可以是多台。
读写分离与 ShardingSphere
就 ShardingSphere 而言,支持主从架构下的读写分离是一项核心功能。目前 ShardingSphere 支持单主库、多从库的主从架构来完成分片环境下的读写分离,暂时不支持多主库的应用场景。
在数据库主从架构中,因为从库一般会有多台,所以当执行一条面向从库的 SQL 语句时我们需要实现一套负载均衡机制来完成对目标从库的路由。ShardingSphere 默认提供了随机Random和轮询RoundRobin这两种负载均衡算法来完成这一目标。
另一方面由于主库和从库之间存在一定的同步时延和数据不一致情况所以在有些场景下我们可能更希望从主库中获取最新数据。ShardingSphere 同样考虑到了这方面需求,开发人员可以通过 Hint 机制来实现对主库的强制路由。
配置读写分离
实现读写分离要做的还是配置工作。通过配置,我们的目标是获取支持读写分离的 MasterSlaveDataSource而 MasterSlaveDataSource 的创建依赖于 MasterSlaveDataSourceFactory 工厂类:
public final class MasterSlaveDataSourceFactory {
public static DataSource createDataSource(final Map<String, DataSource> dataSourceMap, final MasterSlaveRuleConfiguration masterSlaveRuleConfig, final Properties props) throws SQLException {
return new MasterSlaveDataSource(dataSourceMap, new MasterSlaveRule(masterSlaveRuleConfig), props);
}
}
在上面这段代码中,我们可以看到 createDataSource 方法中传入了三个参数,除了熟悉的 dataSourceMap 和 props 之外,还有一个 MasterSlaveRuleConfiguration而这个 MasterSlaveRuleConfiguration 包含了所有我们需要配置的读写分离信息:
public class MasterSlaveRuleConfiguration implements RuleConfiguration {
//读写分离数据源名称
private final String name;
//主库数据源名称
private final String masterDataSourceName;
//从库数据源名称列表
private final List<String> slaveDataSourceNames;
//从库负载均衡算法
private final LoadBalanceStrategyConfiguration loadBalanceStrategyConfiguration;
}
从 MasterSlaveRuleConfiguration 类所定义的变量中不难看出,我们需要配置读写分离数据源名称、主库数据源名称、从库数据源名称列表以及从库负载均衡算法这四个配置项,仅此而已。
系统改造:如何实现读写分离?
在掌握了读写分离的基本概念以及相关配置项之后,我们回到案例,看如何在单库单表架构中引入读写分离机制。
第一步,仍然是设置用于实现读写分离的数据源。为了演示一主多从架构,我们初始化了一个主数据源 dsmaster 以及两个从数据源 dsslave0 和 dsslave1
spring.shardingsphere.datasource.names=dsmaster,dsslave0,dsslave1
spring.shardingsphere.datasource.dsmaster.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.dsmaster.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.dsmaster.url=jdbc:mysql://localhost:3306/dsmaster
spring.shardingsphere.datasource.dsmaster.username=root
spring.shardingsphere.datasource.dsmaster.password=root
spring.shardingsphere.datasource.dsslave0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.dsslave0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.dsslave0.url=jdbc:mysql://localhost:3306/dsslave0
spring.shardingsphere.datasource.dsslave0.username=root
spring.shardingsphere.datasource.dsslave0.password=root
spring.shardingsphere.datasource.dsslave1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.dsslave1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.dsslave1.url=jdbc:mysql://localhost:3306/dsslave1?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
spring.shardingsphere.datasource.dsslave1.username=root
spring.shardingsphere.datasource.dsslave1.password=root
有了数据源之后,我们需要设置 MasterSlaveRuleConfiguration 类中所指定的 4 个配置项,这里负载均衡算法设置的是 random也就是使用的随机算法
spring.shardingsphere.masterslave.name=health_ms
spring.shardingsphere.masterslave.master-data-source-name=dsmaster
spring.shardingsphere.masterslave.slave-data-source-names=dsslave0,dsslave1
spring.shardingsphere.masterslave.load-balance-algorithm-type=random
现在我们来插入 User 对象从控制台的日志中可以看到ShardingSphere 执行的路由类型是 master-slave ,而具体 SQL 的执行是发生在 dsmaster 主库中:
2020-05-25 19:58:08.721 INFO 4392 --- [ main] ShardingSphere-SQL : Rule Type: master-slave
2020-05-25 19:58:08.721 INFO 4392 --- [ main] ShardingSphere-SQL : SQL: INSERT INTO user (user_id, user_name) VALUES (?, ?) ::: DataSources: dsmaster
Insert User:1
2020-05-25 19:58:08.721 INFO 4392 --- [ main] ShardingSphere-SQL : Rule Type: master-slave
2020-05-25 19:58:08.721 INFO 4392 --- [ main] ShardingSphere-SQL : SQL: INSERT INTO user (user_id, user_name) VALUES (?, ?) ::: DataSources: dsmaster
Insert User:2
然后,我们再对 User 对象执行查询操作并获取 SQL 执行日志:
2020-05-25 20:00:33.066 INFO 3364 --- [main] ShardingSphere-SQL : Rule Type: master-slave
2020-05-25 20:00:33.066 INFO 3364 --- [main] ShardingSphere-SQL : SQL : SELECT * FROM user; ::: DataSources: dsslave0
可以看到,这里用到的 DataSource 是 dsslave0也就是说查询操作发生在 dsslave0 从库中。由于设置的是随机负载均衡策略,当我们多次执行查询操作时,目标 DataSource 会在 dsslave0 和 dsslave1 之间交替出现。
系统改造:如何实现读写分离+分库分表?
我们同样可以在分库分表的基础上添加读写分离功能。这时候,我们需要设置两个主数据源 dsmaster0 和 dsmaster1然后针对每个主数据源分别设置两个从数据源
spring.shardingsphere.datasource.names=dsmaster0,dsmaster1,dsmaster0-slave0,dsmaster0-slave1,dsmaster1-slave0,dsmaster1-slave1
这时候的库分片策略 default-database-strategy 同样分别指向 dsmaster0 和 dsmaster1 这两个主数据源:
spring.shardingsphere.sharding.default-database-strategy.inline.sharding-column=user_id
spring.shardingsphere.sharding.default-database-strategy.inline.algorithm-expression=dsmaster$->{user_id % 2}
而对于表分片策略而言,我们还是使用在 07 课时中介绍的分片方式进行设置:
spring.shardingsphere.sharding.tables.health_record.actual-data-nodes=dsmaster$->{0..1}.health_record$->{0..1}
spring.shardingsphere.sharding.tables.health_record.table-strategy.inline.sharding-column=record_id
spring.shardingsphere.sharding.tables.health_record.table-strategy.inline.algorithm-expression=health_record$->{record_id % 2}
完成这些设置之后,同样需要设置两个主数据源对应的配置项:
spring.shardingsphere.sharding.master-slave-rules.dsmaster0.master-data-source-name=dsmaster0
spring.shardingsphere.sharding.master-slave-rules.dsmaster0.slave-data-source-names=dsmaster0-slave0, dsmaster0-slave1
spring.shardingsphere.sharding.master-slave-rules.dsmaster1.master-data-source-name=dsmaster1
spring.shardingsphere.sharding.master-slave-rules.dsmaster1.slave-data-source-names=dsmaster1-slave0, dsmaster1-slave1
这样我们就在分库分表的基础上添加了对读写分离的支持。ShardingSphere 所提供的强大配置体系使得开发人员可以在原有配置的基础上添加新的配置项,而不需要对原有配置做过多调整。
系统改造:如何实现读写分离下的强制路由?
在上个课时中我们介绍了强制路由,在这个基础上,我将给出如何基于 Hint完成读写分离场景下的主库强制路由方案。
要想实现主库强制路由,我们还是要使用 HintManager。HintManager 专门提供了一个 setMasterRouteOnly 方法,用于将 SQL 强制路由到主库中。我们把这个方法也封装在 HintManagerHelper 帮助类中:
public class HintManagerHelper {
static void initializeHintManagerForMaster(final HintManager hintManager) {
hintManager.setMasterRouteOnly();
}
}
现在,我们在业务代码中加入主库强制路由的功能,下面这段代码演示了这个过程:
@Override
public void processWithHintValueMaster() throws SQLException, IOException {
DataSource dataSource = DataSourceHelper.getDataSourceForMaster();
try (HintManager hintManager = HintManager.getInstance();
Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement()) {
HintManagerHelper.initializeHintManagerForMaster(hintManager);
ResultSet result = statement.executeQuery("select user_id, user_name from user");
while (result.next()) {
System.out.println(result.getLong(1) + ": " + result.getString(2));
}
}
}
执行这段代码,可以在控制台日志中获取执行的结果:
2020-05-25 22:06:17.166 INFO 16680 --- [ main] ShardingSphere-SQL : Rule Type: master-slave
2020-05-25 22:06:17.166 INFO 16680 --- [ main] ShardingSphere-SQL : SQL: select user_id, user_name from user ::: DataSources: dsmaster
1: user_1
2: user_2
显然,这里的路由类型是 master-slave而执行 SQL 的 DataSource 只有 dsmaster也就是说我们完成了针对主库的强制路由。
小结
继续承接上一课时的内容,今天我们讲解 ShardingSphere 中的读写分离机制。在日常开发过程中读写分离是应对高并发数据访问的一种有效技术手段。而在ShardingSphere中读写分离既可以单独使用也可以和分库组合在一起使用。ShardingSphere的另一个强大之处还在于提供了针对主库的强制路由机制这在需要确保获取主库最新数据的场景下非常有用。
这里给你留一道思考题:如果我们想要在主从架构中只访问主库中的数据,在 ShardingSphere 中有什么方法可以做到这一点?

View File

@ -0,0 +1,395 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
34 系统集成:如何完成 ShardingSphere 内核与 Spring+SpringBoot 的无缝整合?
今天,我们将进入整个课程中最后一个模块——系统集成模块的介绍。这里所谓的系统集成,指的就是 ShardingSphere 和 Spring 框架的集成。
到目前为止ShardingSphere 实现了两种系统集成机制一种是命名空间namespace机制即通过扩展 Spring Schema 来实现与 Spring 框架的集成;而另一种则是通过编写自定义的 starter 组件来完成与 Spring Boot 的集成。本课时我将分别讲解这两种系统集成机制。
基于系统集成模块,无论开发人员采用哪一种 Spring 框架,对于使用 ShardingSphere 而言都是零学习成本。
基于命名空间集成 Spring
从扩展性的角度讲,基于 XML Schema 的扩展机制也是非常常见和实用的一种方法。在 Spring 中,允许我们自己定义 XML 的结构,并且可以用自己的 Bean 解析器进行解析。通过对 Spring Schema 的扩展ShardingSphere 可以完成与 Spring 框架的有效集成。
1.基于命名空间集成 Spring 的通用开发流程
基于命名空间机制实现与 Spring 的整合,开发上通常采用的是固定的一个流程,包括如下所示的五大步骤:
这些步骤包括:编写业务对象、编写 XSD 文件、编写 BeanDefinitionParser 实现类、编写 NamespaceHandler 实现类,以及编写 spring.handlers 和 spring.schemas 配置文件,我们来看看 ShardingSphere 中实现这些步骤的具体做法。
2.ShardingSphere 集成 Spring
ShardingSphere 中存在两个以“spring-namespace”结尾的代码工程即 sharding-jdbc-spring-namespace 和 sharding-jdbc-orchestration-spring-namespace显然后者关注的是编排治理相关功能的集成相对比较简单。再因为命名空间机制的实现过程也基本一致因此我们以 sharding-jdbc-spring-namespace 工程为例展开讨论。
而在 sharding-jdbc-spring-namespace 工程中,又包含了对普通分片、读写分离和数据脱敏这三块核心功能的集成内容,它们的实现也都是采用了类似的方式,因此我们也不会重复进行说明,这里就以普通分片为例进行介绍。
首先,我们发现了一个专门用于与 Spring 进行集成的 SpringShardingDataSource 类,这个类就是业务对象类,如下所示:
public class SpringShardingDataSource extends ShardingDataSource {
public SpringShardingDataSource(final Map<String, DataSource> dataSourceMap, final ShardingRuleConfiguration shardingRuleConfiguration, final Properties props) throws SQLException {
super(dataSourceMap, new ShardingRule(shardingRuleConfiguration, dataSourceMap.keySet()), props);
}
}
可以看到这个 SpringShardingDataSource 类实际上只是对 ShardingDataSource 的一种简单封装,没有包含任何实际操作。
然后我们来看配置项标签的定义类这种类是一种简单的工具类其作用就是定义标签的名称。在命名上ShardingSphere 中的这些类都以“BeanDefinitionParserTag”结尾例如如下所示的 ShardingDataSourceBeanDefinitionParserTag
public final class ShardingDataSourceBeanDefinitionParserTag {
public static final String ROOT_TAG = "data-source";
public static final String SHARDING_RULE_CONFIG_TAG = sharding-rule";
public static final String PROPS_TAG = "props";
public static final String DATA_SOURCE_NAMES_TAG = "data-source-names";
public static final String DEFAULT_DATA_SOURCE_NAME_TAG = "default-data-source-name";
public static final String TABLE_RULES_TAG = "table-rules";
}
这里定义了一批 Tag 和一批 Attribute我们不做 一 一 展开。可以对照如下所示的基于 XML 的配置示例来对这些定义的配置项进行理解:
<sharding:data-source id="shardingDataSource">
<sharding:sharding-rule data-source-names="ds0,ds1">
<sharding:table-rules>
<sharding:table-rule />
<sharding:table-rule />
</sharding:table-rules>
</sharding:sharding-rule>
</sharding:data-source>
然后,我们在 sharding-jdbc-spring-namespace 代码工程的 META-INF/namespace 文件夹下找到了对应的 sharding.xsd 文件,其基本结构如下所示:
<xsd:schema xmlns="http://shardingsphere.apache.org/schema/shardingsphere/sharding"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:encrypt="http://shardingsphere.apache.org/schema/shardingsphere/encrypt"
targetNamespace="http://shardingsphere.apache.org/schema/shardingsphere/sharding"
elementFormDefault="qualified" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://shardingsphere.apache.org/schema/shardingsphere/encrypt http://shardingsphere.apache.org/schema/shardingsphere/encrypt/encrypt.xsd">
<xsd:import namespace="http://www.springframework.org/schema/beans" schemaLocation="http://www.springframework.org/schema/beans/spring-beans.xsd" />
<xsd:import namespace="http://shardingsphere.apache.org/schema/shardingsphere/encrypt" schemaLocation="http://shardingsphere.apache.org/schema/shardingsphere/encrypt/encrypt.xsd"/>
<xsd:element name="data-source">
<xsd:complexType>
<xsd:all>
<xsd:element ref="sharding-rule" />
<xsd:element ref="props" minOccurs="0" />
</xsd:all>
<xsd:attribute name="id" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:schema>
可以看到对于“data-source”这个 element 而言包含了“sharding-rule”和“props”这两个子 element其中“props”不是必需的。同时“data-source”还可以包含一个“id”属性而这个属性则是必填的我们在前面的配置示例中已经看到了这一点。而对于“sharding-rule”而言则可以有很多内嵌的属性sharding.xsd 文件中对这些属性都做了定义。
同时我们应该注意到的是sharding.xsd 中通过使用 xsd:import 标签还引入了两个 namespace一个是 Spring 中的http://www.springframework.org/schema/beans另一个则是 ShardingSphere 自身的http://shardingsphere.apache.org/schema/shardingsphere/encrypt这个命名空间的定义位于与 sharding.xsd 同目录下的 encrypt.xsd文件中。
有了业务对象类,以及 XSD 文件的定义,接下来我们就来看看 NamespaceHandler 实现类 ShardingNamespaceHandler如下所示
public final class ShardingNamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
registerBeanDefinitionParser(ShardingDataSourceBeanDefinitionParserTag.ROOT_TAG, new ShardingDataSourceBeanDefinitionParser());
registerBeanDefinitionParser(ShardingStrategyBeanDefinitionParserTag.STANDARD_STRATEGY_ROOT_TAG, new ShardingStrategyBeanDefinitionParser());
}
}
可以看到这里也是直接使用了 registerBeanDefinitionParser 方法来完成标签项与具体的 BeanDefinitionParser 类之间的对应关系。我们来看这里的 ShardingDataSourceBeanDefinitionParser其核心的 parseInternal 方法如下所示:
@Override
protected AbstractBeanDefinition parseInternal(final Element element, final ParserContext parserContext) {
//构建针对 SpringShardingDataSource 的 BeanDefinitionBuilder
BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(SpringShardingDataSource.class);
//解析构造函数中的 DataSource 参数
factory.addConstructorArgValue(parseDataSources(element));
//解析构造函数中 ShardingRuleConfiguration 参数 factory.addConstructorArgValue(parseShardingRuleConfiguration(element));
//解析构造函数中 Properties 参数
factory.addConstructorArgValue(parseProperties(element, parserContext));
factory.setDestroyMethodName("close");
return factory.getBeanDefinition();
}
这里,我们自己定义了一个 BeanDefinitionBuilder 并将其绑定到前面定义的业务对象类 SpringShardingDataSource。然后我们通过三个 addConstructorArgValue 方法的调用,分别为 SpringShardingDataSource 构造函数中所需的 dataSourceMap、shardingRuleConfiguration 以及 props 参数进行赋值。
我们再来进一步看一下上述方法中的 parseDataSources 方法,如下所示:
private Map<String, RuntimeBeanReference> parseDataSources(final Element element) {
Element shardingRuleElement = DomUtils.getChildElementByTagName(element, ShardingDataSourceBeanDefinitionParserTag.SHARDING_RULE_CONFIG_TAG);
List<String> dataSources = Splitter.on(",").trimResults().splitToList(shardingRuleElement.getAttribute(ShardingDataSourceBeanDefinitionParserTag.DATA_SOURCE_NAMES_TAG));
Map<String, RuntimeBeanReference> result = new ManagedMap<>(dataSources.size());
for (String each : dataSources) {
result.put(each, new RuntimeBeanReference(each));
}
return result;
}
基于前面介绍的配置示例我们理解这段代码的作用是获取所配置的“ds0,ds1”字符串并对其进行拆分然后基于每个代表具体 DataSource 的名称构建 RuntimeBeanReference 对象并进行返回,这样就可以把在 Spring 容器中定义的其他 Bean 加载到 BeanDefinitionBuilder 中。
关于 ShardingDataSourceBeanDefinitionParser 中其他 parse 方法的使用,大家可以通过阅读对应的代码进行理解,处理方式都是非常类似的,就不再重复展开。
最后,我们需要在 META-INF 目录下提供spring.schemas 文件,如下所示:
http\://shardingsphere.apache.org/schema/shardingsphere/sharding/sharding.xsd=META-INF/namespace/sharding.xsd
http\://shardingsphere.apache.org/schema/shardingsphere/masterslave/master-slave.xsd=META-INF/namespace/master-slave.xsd
http\://shardingsphere.apache.org/schema/shardingsphere/encrypt/encrypt.xsd=META-INF/namespace/encrypt.xsd
同样spring.handlers 的内容如下所示:
http\://shardingsphere.apache.org/schema/shardingsphere/sharding=org.apache.shardingsphere.shardingjdbc.spring.namespace.handler.ShardingNamespaceHandler
http\://shardingsphere.apache.org/schema/shardingsphere/masterslave=org.apache.shardingsphere.shardingjdbc.spring.namespace.handler.MasterSlaveNamespaceHandler
http\://shardingsphere.apache.org/schema/shardingsphere/encrypt=org.apache.shardingsphere.shardingjdbc.spring.namespace.handler.EncryptNamespaceHandler
至此,我们对 ShardingSphere 中基于命名空间机制与 Spring 进行系统集成的实现过程介绍完毕。
接下来,我们来看 ShardingSphere 中实现一个自定义 spring-boot-starter 的过程。
基于自定义 starter 集成 Spring Boot
与基于命名空间的实现方式一样ShardingSphere 提供了 sharding-jdbc-spring-boot-starter 和 sharding-jdbc-orchestration-spring-boot-starter 这两个 starter 工程。篇幅关系,我们同样只关注于 sharding-jdbc-spring-boot-starter 工程。
对于 Spring Boot 工程,我们首先来关注 META-INF 文件夹下的 spring.factories 文件。Spring Boot 中提供了一个 SpringFactoriesLoader 类,该类的运行机制类似于 “13 | 微内核架构ShardingSphere如何实现系统的扩展性” 中所介绍的 SPI 机制,只不过以服务接口命名的文件是放在 META-INF/spring.factories 文件夹下,对应的 Key 为 EnableAutoConfiguration。SpringFactoriesLoader 会查找所有 META-INF/spring.factories 目录下的配置文件,并把 Key 为 EnableAutoConfiguration 所对应的配置项通过反射实例化为配置类并加载到容器。在 sharding-jdbc-spring-boot-starter 工程中,该文件内容如下所示:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.apache.shardingsphere.shardingjdbc.spring.boot.SpringBootConfiguration
现在这里的 EnableAutoConfiguration 配置项指向了 SpringBootConfiguration 类。也就是说,这个类在 Spring Boot 启动过程中都能够通过 SpringFactoriesLoader 被加载到运行时环境中。
1.SpringBootConfiguration 中的注解
接下来,我们就来到这个 SpringBootConfiguration首先关注于加在该类上的各种注解如下所示
@Configuration
@ComponentScan("org.apache.shardingsphere.spring.boot.converter")
@EnableConfigurationProperties({
SpringBootShardingRuleConfigurationProperties.class,
SpringBootMasterSlaveRuleConfigurationProperties.class, SpringBootEncryptRuleConfigurationProperties.class, SpringBootPropertiesConfigurationProperties.class})
@ConditionalOnProperty(prefix = "spring.shardingsphere", name = "enabled", havingValue = "true", matchIfMissing = true)
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
@RequiredArgsConstructor
public class SpringBootConfiguration implements EnvironmentAware
首先,我们看到了一个 @Configuration 注解。这个注解不是 Spring Boot 引入的新注解,而是属于 Spring 容器管理的内容。该注解表明这个类是一个配置类,可以启动组件扫描,用来将带有 @Bean 注解的实体进行实例化 bean。
然后,我们又看到了一个同样属于 Spring 容器管理范畴的老注解,即 @ComponentScan 注解。@ComponentScan 注解就是扫描基于 @Component 等注解所标注的类所在包下的所有需要注入的类,并把相关 Bean 定义批量加载到IoC容器中。
显然Spring Boot 应用程序中同样需要这个功能。注意到,这里需要进行扫描的包路径位于另一个代码工程 sharding-spring-boot-util 的 org.apache.shardingsphere.spring.boot.converter 包中。
然后,我们看到了一个 @EnableConfigurationProperties 注解,该注解的作用就是使添加了 @ConfigurationProperties 注解的类生效。在 Spring Boot 中,如果一个类只使用了 @ConfigurationProperties 注解,然后该类没有在扫描路径下或者没有使用 @Component 等注解,就会导致无法被扫描为 bean那么就必须在配置类上使用 @EnableConfigurationProperties 注解去指定这个类,才能使 @ConfigurationProperties 生效,并作为一个 bean 添加进 spring 容器中。这里的 @EnableConfigurationProperties 注解包含了四个具体的 ConfigurationProperties。以 SpringBootShardingRuleConfigurationProperties 为例,该类的定义如下所示,可以看到,这里直接继承了 sharding-core-common 代码工程中的 YamlShardingRuleConfiguration
@ConfigurationProperties(prefix = "spring.shardingsphere.sharding")
public class SpringBootShardingRuleConfigurationProperties extends YamlShardingRuleConfiguration {
}
SpringBootConfiguration 上的下一个注解是 @ConditionalOnProperty,该注解的作用在于只有当所提供的属性属于 true 时才会实例化 Bean。
最后一个与自动加载相关的注解是 @AutoConfigureBefore,如果该注解用在类名上,其作用是标识在加载当前类之前需要加载注解中所设置的配置类。基于这一点,我们明确在加载 SpringBootConfiguration 类之前Spring Boot 会先加载 DataSourceAutoConfiguration。这一步的作用与我们后面要看到的创建各种 DataSource 相关。
2.SpringBootConfiguration 中的功能
介绍完这些注解之后,我们来看一下 SpringBootConfiguration 类所提供的功能。
我们知道对于 ShardingSphere 而言,其对外的入口实际上就是各种 DataSource因此 SpringBootConfiguration 中提供了一批创建不同 DataSource 的入口方法,例如如下所示的 shardingDataSource 方法:
@Bean
@Conditional(ShardingRuleCondition.class)
public DataSource shardingDataSource() throws SQLException {
return ShardingDataSourceFactory.createDataSource(dataSourceMap, new ShardingRuleConfigurationYamlSwapper().swap(shardingRule), props.getProps());
}
该方法上添加了两个注解,一个是常见的 @Bean,另一个则是 @Conditional 注解,该注解的作用是只有满足指定条件的情况下才能加载这个 Bean。我们看到 @Conditional 注解中设置了一个 ShardingRuleCondition该类如下所示
public final class ShardingRuleCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(final ConditionContext conditionContext, final AnnotatedTypeMetadata annotatedTypeMetadata) {
boolean isMasterSlaveRule = new MasterSlaveRuleCondition().getMatchOutcome(conditionContext, annotatedTypeMetadata).isMatch();
boolean isEncryptRule = new EncryptRuleCondition().getMatchOutcome(conditionContext, annotatedTypeMetadata).isMatch();
return isMasterSlaveRule || isEncryptRule ? ConditionOutcome.noMatch("Have found master-slave or encrypt rule in environment") : ConditionOutcome.match();
}
}
可以看到 ShardingRuleCondition 是一个标准的 SpringBootCondition实现了 getMatchOutcome 抽象方法。我们知道 SpringBootCondition 的作用就是代表一种用于注册类或加载 Bean 的条件。ShardingRuleCondition 类的实现上分别调用了 MasterSlaveRuleCondition 和 EncryptRuleCondition 来判断是否满足这两个 SpringBootCondition。显然对于 ShardingRuleCondition 而言,只有在两个条件都不满足的情况下才应该被加载。对于 masterSlaveDataSource 和 encryptDataSource 这两个方法而言,处理逻辑也类似,不做赘述。
最后,我们注意到 SpringBootConfiguration 还实现了 Spring 的 EnvironmentAware 接口。在 Spring Boot 中,当一个类实现了 EnvironmentAware 接口并重写了其中的 setEnvironment 方法之后,在代码工程启动时就可以获得 application.properties 配置文件中各个配置项的属性值。SpringBootConfiguration 中所重写的 setEnvironment 方法如下所示:
@Override
public final void setEnvironment(final Environment environment) {
String prefix = "spring.shardingsphere.datasource.";
for (String each : getDataSourceNames(environment, prefix)) {
try {
dataSourceMap.put(each, getDataSource(environment, prefix, each));
} catch (final ReflectiveOperationException ex) {
throw new ShardingException("Can't find datasource type!", ex);
} catch (final NamingException namingEx) {
throw new ShardingException("Can't find JNDI datasource!", namingEx);
}
}
}
这里的代码逻辑是获取“spring.shardingsphere.datasource.name”或“spring.shardingsphere.datasource.names”配置项然后根据该配置项中所指定的 DataSource 信息构建新的 DataSource 并加载到 dataSourceMap 这个 LinkedHashMap。这点我们可以结合课程案例中的配置项来加深理解
spring.shardingsphere.datasource.names=ds0,ds1
spring.shardingsphere.datasource.ds0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.url=jdbc:mysql://localhost/ds0
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=root
spring.shardingsphere.datasource.ds1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds1.url=jdbc:mysql://localhost/ds1
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.password=root
至此,整个 SpringBootConfiguration 的实现过程介绍完毕。
从源码解析到日常开发
今天所介绍的关于 ShardingSphere 集成 Spring 的实现方法可以直接导入到日常开发过程中。如果我们需要实现一个自定义的框架或工具类,从面向开发人员的角度讲,最好能与 Spring 等主流的开发框架进行集成,以便提供最低的学习和维护成本。与 Spring 框架的集成过程都有固定的开发步骤,我们按照今天课时中所介绍的内容,就可以模仿 ShardingSphere 中的做法自己实现这些步骤。
小结与预告
本课时是 ShardingSphere 源码解析的最后一部分内容,我们围绕如何集成 Spring 框架这一主题对 ShardingSphere 的具体实现方法做了展开。ShardingSphere 在这方面提供了一种可以直接进行参考的模版式的实现方法,包括基于命名空间的 Spring 集成以及基于 starter的Spring Boot 集成方法。
这里给你留一道思考题:在 ShardingSphere 集成 Spring Boot 时SpringBootConfiguration 类上的注解有哪些,分别起到了什么作用?
讲完 ShardingSphere 源码解析部分内容之后,下一课时是整个课程的最后一讲,我们将对 ShardingSphere 进行总结,并对它的后续发展进行展望。