通过前面几个课时的介绍,相信你对 ShardingSphere 已经有了初步了解。从今天开始,我将带领你通过案例分析逐步掌握 ShardingSphere 的各项核心功能,首当其冲的就是分库分表机制。

单库单表系统

我们先从单库单表系统说起。在整个课程中,如果没有特殊强调,我们将默认使用 Spring Boot 集成和 ShardingSphere 框架,同时基于 Mybatis 实现对数据库的访问。

导入开发框架

系统开发的第一步是导入所需的开发框架。在下面这段代码中,我们新建了一个 Spring Boot 代码工程,在 pom 文件中需要添加对 sharding-jdbc-spring-boot-starter 和 mybatis-spring-boot-starter 这两个 starter 的引用:

1
2
3
4
5
6
7
8
9
<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 配置文件中。

1
2
3
4
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,我们可以完成不同配置体系之间的切换。

1
spring.profiles.active=traditional

接下来要做的事情就是创建 Repository 层组件:

Repository 层接口定义

最后,我们设计并实现了相关的三个服务类,分别是 UserService、HealthLevelService 和 HealthRecordService。

Service 层接口和实现类定义

通过 UserService,我们会插入一批用户数据用于完成用户信息的初始化。然后,我们有一个 HealthLevelService,专门用来初始化健康等级信息。请注意,与其他业务对象不同,健康等级信息是系统中的一种典型字典信息,我们假定系统中存在 5 种健康等级。

第三个,也是最重要的服务就是 HealthRecordService,我们用它来完成 HealthRecord 以及 HealthTask 数据的存储和访问。这里以 HealthRecordService 服务为例,下面这段代码给出了它的实现过程:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@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 等常见单元测试注解的使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@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 工厂类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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

设置分片策略

明确了数据源之后,我们需要设置针对分库的分片策略:

1
2
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:

1
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 条,它们呈现为笛卡尔积:

1
2
3
4
5
6
7
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 条:

1
2
3
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 应该互为绑定表关系。所以,我们可以在配置文件中添加对这种关系的配置:

1
spring.shardingsphere.sharding.binding-tables=health_record, health_task

介绍完绑定表,再来看广播表的概念。所谓广播表(BroadCastTable),是指所有分片数据源中都存在的表,也就是说,这种表的表结构和表中的数据在每个数据库中都是完全一样的。广播表的适用场景比较明确,通常针对数据量不大且需要与海量数据表进行关联查询的应用场景,典型的例子就是每个分片数据库中都应该存在的字典表。

同样回到我们的场景,对于 health_level 表而言,由于它保存着有限的健康等级信息,可以认为它就是这样的一种字典表。所以,我们也在配置文件中添加了对广播表的定义,在下面这段代码中你可以看到:

1
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 配置也就顺理成章了:

1
2
3
4
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 的具体数据列:

1
2
3
4
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 中分片引擎的核心功能之一,也可以说是最简单的功能之一。在下一课时中,我们将继续介绍分表、分库+分表以及强制路由等分片机制。

-– ### 精选评论 ##### **浩: > 我感觉讲解的很好,如果能把文章中对应的工程代码提供出来就更好啦~ ######     编辑回复: >     源码都在github上:https://github.com/tianyilan12/shardingsphere-demo ##### *兵: > 分库之后如何解决关联查询和模糊查询呢 ######     讲师回复: >     ShardingSphere对于普通的关联查询和模糊查询是透明的,会基于输入的SQL语句自动从各个库中获取目标数据进行归并。当然,对有些SQL语句是有限制的,官网上有说明。 ##### **潮: > 未分片时向数据库中插入式局报错解决方法,把spring.shardingsphere.enable设置为false。因为开启它的话好像读取数据库相关信息的方式变了,需要通过 spring.shardingsphere.datasource来设定。。 ######     讲师回复: >     对的,如果不分片就不要启动shardingsphere,shardingsphere会拦截原有sql做对应的处理 ##### *波: > 配置了分库策略spring.shardingsphere.sharding.default-database-strategy.inline.algorithm-expression=ds$-{user_id % 2}但是发现,在 ds0 中的 user表中居然有user_id=7和user_id=9的数据。按道理是应该出现在 ds1中的呀。这是怎么回事呢? ##### **超: > 老师,您好,我按照您的操作遇到了个 ERROR 10002(C1000):2Unknown exception:[null],请教下 这个是什么原因导致的,一直没查到解决办法?另外 只分表不分库 但有些表是单表的,该如何配置? ######     讲师回复: >     具体问题要看一下,只分表当然是可以的,我们的课程里有相关的描述和示例代码 ##### **峰: > mysql 我能登录上去。 ##### *亚: > 请问老师,ShardingSphere不支持批量更新吗? 有什么折中解决办法 ######     讲师回复: >     应该是支持批量更新的 ##### **国: > 運行這個spring.profiles.active=traditional報錯了,哪裏問題?Caused by: java.lang.IllegalArgumentException: Data sources cannot be empty. ######     讲师回复: >     确认一下数据库地址是否设置正确 ##### **初: > 老师您好,在做saas系统时,只分库不分表,但是配置强制路由策略时,需要绑在具体表的策略,如果表多的时候就需要配置很多信息,有没有更简单的方法,只需要配置分库的强制路由策略即可,不用配置具体的表呢? ######     讲师回复: >     强制路由本质上就是数据库提供的Hint机制,我理解Hint机制应该是要和表一起才能运作的 ##### **用户1024: > Failed to configure a DataSource: ‘url’ attribute is not specified and no embedded datasource could be configured.源码报这个问题 ,但实际我看都配置了 ######     讲师回复: >     是哪个代码示例出现问题? ##### **坤: > 老师你好,请教三个问题:1、对存量数据大表如订单表,怎么通过ShardingSphere进行分库分表存储?2、类比此小节demo,如果同时存在多对绑定表 如health_record/health_task采用user_id分片、order/order_item采用order_id分片,那么分片键 要分别怎么指定,因为感觉spring.shardingsphere.sharding.default-database-strategy.inline.sharding-column 在存在一对绑定表的话还好说,如果存在多对绑定表这种设置方式不能将分片键与具体相关的绑定表进行联系3、类比此小节demo,health_record/health_task 参与分片因而进行分片设置,可如果user表 不需要进行分库分表,那么要怎么处理?当前直接操作user 进行插入是会报错的【Missing the data source name: ’null’】 ######     讲师回复: >     1.这个直接使用ShardingSphere的分库分表功能即可 2.分别设置多组绑定表和分片键 3.应该是可以直接插入user的,报错感觉是数据源设置问题,不是机制问题 ##### **辉: > user表不需要分库么 ######     讲师回复: >     这里案例是演示的是health_record和health_task的分库,不是针对user表的,user表当然也可以分库 ##### *晨: > 老师,大表的数据也会分表自动迁移过去吗 ######     讲师回复: >     分表就是为了解决大表的数据咯 ##### *涛: > key-generator.props.worker.id是干什么用的,为什么是33 ######     讲师回复: >     这是是雪花算法中的一个参数,用于指定workerid的,这个33只是一个示例,你可以根据自己需要设置,可以参考第14课时的内容 ##### **晖: > 添加了shardingsphere以后怎么通过mybatis @DataSource手动切换数据源 ######     讲师回复: >     这个我理解所有DataSource都应该由ShardingSphere来统一管理了,这样才能保证数据的一致性,不应该再通过其他第三方工具类切换数据源 ##### **4562: > 分表分库之后怎么分页呢 ######     讲师回复: >     ShardingSphere自动集成了分页机制的,可以看后面的“归并引擎”相关的内容 ##### **5052: > 我看了一下绑定表的配置,如果存在多个绑定表的话,配置起来不是非常的多? ######     讲师回复: >     是的,针对每个绑定表都需要配置。一般一个系统中,真正用到分库分表的表不会太多的,太多的话就不适合用关系型数据库了,所以绑定表也都有限。 ##### **坤: > Missing the data source name: ’null’这个错的原因是需要插入数据的表没有在 “spring.shardingsphere.sharding.tables” 配置对应的表 ######     讲师回复: >     这个datasource name为null应该是数据库不存在,不是数据表不存在 ##### **威: > 原模原样的代码拉下来跑不了啊,2020-10-08 20:21:12.839 nested exception is java.lang.IllegalArgumentException: qualifiedColumns is required. ######     讲师回复: >     我们的demo中是有一些SQL脚本的,需要先执行。请同学在跑代码之前执行一下数据库脚本。 ##### *啊: > 如何做垂直分库后的跨库连表查询? ######     讲师回复: >     基本的跨库连表查询ShardingSphere都已经帮忙处理好了,内部使用了路由和归并等机制,但并不是所有SQL都可以做得到,可以看一下官网上的使用说明,上面有使用上的一些限制条件 ##### *啊: > 请问,如何做到只分库不分表?如order表只在订单库,goods表只在商品库 ######     讲师回复: >     只分库的话就采用只采用文章中提到的分库策略就可以了,是最简单的一种方式 ##### *啊: > 绑定表设置:如果a、b互为绑定表 c、d互为绑定表,怎么设置呢? ######     讲师回复: >     就分别设置两对绑定表关系就好了,a和b是一对,c和d是一对 ##### **平: > 提供的源码 traditional配置文件也要指定datasource.names等属性,不然会报datasource is empty ######     讲师回复: >     对的,如果用到这个配置文件的话也要配置的 ##### **2538: > 我在使用application-sharding-databases.properties配置文件的时候,报了以下错误,请帮忙看看是什么原因:Exception in thread “main” org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:### Error updating database. Cause: java.lang.IllegalStateException: Missing the data source name: ’null’### The error may involve defaultParameterMap### The error occurred while setting parameters### SQL: INSERT INTO user (user_id, user_name) VALUES (?, ?)### Cause: java.lang.IllegalStateException: Missing the data source name: ’null’ at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:79) ######     讲师回复: >     这个应该还是第三方的数据源的实现库引用有问题,大概率数据源工具版本问题 ##### **9434: > org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘dataSource’ defined in class path resource [org/apache/shardingsphere/shardingjdbc/spring/boot/SpringBootConfiguration.class]请教下这个DataSource为空是什么原因?代码从git上拉下来的 ######     讲师回复: >     这个问题很大程度是所依赖的第三方DataSource库版本的问题,确认一下git上拉下来的代码中执行的是哪部分功能,不要修改第三方工具库的版本 ##### **王: > 按课程配置完 insert的时候报空指针,应该是找不到driverClassName,看不出来啥问题😂 ######     讲师回复: >     找不到driverClassName的话,需要在maven中添加Druid等相关的第三方框架。如果添加之后还是报错,那需要注意框架的版本。如果框架已经添加了版本也和案例中的一致,应该是可以运行的。 ##### **开挂: > 这章节中health_record和health_task中的数据都是根据userId对2取模进行分库的吧?user表中插入数据是随机分配的? ######     讲师回复: >     是的,是对userId对2取模,user表中插入的数据是模拟的数据,代码中有具体的数据构建方式 ##### **系是深井冰: > health_level表在新增的时候会同时插入到多个库里面么? ######     讲师回复: >     是的 ##### *欢: > 请教老师,如果说绑定表的分片键必须一致的话,那为什么还要特别指明绑定关系呢?直接用各自的分片键直接判断不就好了吗。 ######     讲师回复: >     各自用分片键的话只是针对单独一个表,设置绑定表的目的就是几张表通过同一个分片键统一操作