主要组件版本信息:
SpringBoot:2.2.8.RELEASE
MyBatis Plus:3.3.2
ShardingSphere:4.0.0-RC2
需求说明
在企业开发中,如果业务数据分布在不同的数据源,那么我们就希望在访问业务数据的时候,能够根据业务需求,动态地切换数据源,ShardingSphere
是一款不错的数据库中间件,利用它,可以很方便地实现我们想要的功能,下面,我们从零开始介绍,项目搭建及多数据源切换实现。
技术选型
Java 8 + MySql 5.7+ SpringBoot + Lombok + Mybatis Plus + ShardingSphere
开发工具:IntelliJ IDEA + Navicat
SpringBoot项目搭建
打开IDEA,新建一个SpringBoot 项目,如下图示:
填写完项目元数据,点击
Next
继续下一步,
由以上步骤可以看到,用
IDEA
搭建SpringBoots项目非常方便。
项目创建完成后,我们来看下整体目录结构,如下图示:
我们调整下pom.xml
,改成如下所示:
4.0.0
com.dgd
multi-datasource
1.0.0-SNAPSHOT
multi-datasource
多数据源切换
UTF-8
1.8
2.2.8.RELEASE
org.springframework.boot
spring-boot-starter
${springboot.version}
org.springframework.boot
spring-boot-starter-web
${springboot.version}
org.springframework.boot
spring-boot-starter-test
test
${springboot.version}
org.springframework.boot
spring-boot-maven-plugin
org.apache.maven.plugins
maven-compiler-plugin
3.5.1
1.8
spring-boot-starter
是SpringBoot项目的核心,必须要引入;spring-boot-starter-web
提供了web相关功能,而spring-boot-starter-test
是SpringBoot的测试组件,后续我们写单元测试会用到它。
下面我们来写个HelloWorld
接口,验证一下项目搭建是否没问题。
代码如下:
package com.dgd.multidatasource.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* @author : DaiGD
* @createtime : 2020年06月13日 14:17
* @description : HelloWorld 控制器
*/
@RestController
public class HelloWorldController
{
@GetMapping("/hello/{userName}")
public String helloWorld(@PathVariable String userName)
{
return "Hello:" + userName;
}
}
新建包并取命为:com.dgd.multidatasource.controller
;新建类并取名为:HelloWorldController
,在类上添加注解@RestController
,该注解将帮助我们创建REST风格的web服务,具体讲解参看此;写一个方法名为:helloWorld
,方法上添加注解GetMapping
,表明该方法只接收GET
请求,入参上添加注解@PathVariable
,它将帮我们读取到请求路径上定义的userName
参数。此时我们的项目如下图示:
接下来我们把项目启动,回到MultiDatasourceApplication
类,点击绿色小图标,选择Run
选项,启动项目,如图示:
看到控制台输出如下日志,表明项目启动没问题:
接着,我们在浏览器地址栏上输入http://localhost:8080/hello/Dannis
,看到网页上出现Hello:Dannis
,表明SpringBoot项目成功搭建完成。
数据初始化
现在我们来创建两个数据源,真实场景的多数据源,数据库所在的服务器一般是不相同的,如果是为了模拟真实环境,我们可以在自己电脑上搭建两个虚拟机,分别搭建数据库,或者利用Docker来创建两个数据库,或者买两个云服务器,分别在上面搭建两个数据库,为了简单起见,也可以是在同一个MySql服务上创建两个不同的库,我们就按最后一种情况来,假设已在本地上安装好MySql服务环境,接下来,我们用下面的脚本命令来初始化我们的测试数据:
# 创建第一个数据源
DROP DATABASE IF EXISTS `ds_01`;
CREATE DATABASE `ds_01`;
# 创建用户表并初始化数据
DROP TABLE IF EXISTS `ds_01`.`user`;
CREATE TABLE `ds_01`.`user` (
`id` BIGINT(11) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
`user_name` VARCHAR(16) NOT NULL COMMENT '用户名'
);
INSERT INTO `ds_01`.`user` (`user_name`) VALUES
('Dannis'),
('小飞飞');
# 创建第二个数据源
DROP DATABASE IF EXISTS `ds_02`;
CREATE DATABASE `ds_02`;
# 创建订单表并初始化数据
DROP TABLE IF EXISTS `ds_02`.`order`;
CREATE TABLE `ds_02`.`order` (
`id` BIGINT(11) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID',
`user_id` BIGINT(11) NOT NULL COMMENT '用户ID',
`address` VARCHAR(32) NOT NULL COMMENT '收货地址'
);
INSERT INTO `ds_02`.`order` (`user_id`,`address`) VALUES
(1,'北京市朝阳区'),
(2,'广州市海珠区');
SQL脚本执行完毕,点击localhost
鼠标右键选择刷新,然后可看到出现两个数据库ds_01
和ds_02
,打开查看一下,发现数据已正常写入,如下图所示:
利用Mybatis Plus来访问数据
Mybatis Plus是ORM框架MyBatis
的增强版,具体介绍可查看官网。
这里我们选用它来简化对数据库的操作,同时,我们也引入Lombok
插件来简化Java对象相关方法的编码(IDEA需提前安装好Lombok
插件并添加相关配置,具体步骤可自行百度),在pom.xml
添加如下代码:
配置版本号:
UTF-8
1.8
2.2.8.RELEASE
1.18.4
5.1.42
3.3.2
引入依赖:
mysql
mysql-connector-java
${mysql-connector-java.version}
com.baomidou
mybatis-plus-boot-starter
${mybatis-plus.version}
org.projectlombok
lombok
${lombok.version}
新增包并命名为com.dgd.multidatasource.model.mybatis.entity
,
新建User
类,代码如下:
package com.dgd.multidatasource.model.mybatis.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
/**
* @author : DaiGD
* @createtime : 2020年06月13日 15:33
* @description : 用户表
*/
@Data
@TableName("`user`")
public class User implements Serializable
{
private Long id;
private String userName;
}
新建Order
类,代码如下:
package com.dgd.multidatasource.model.mybatis.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
/**
* @author : DaiGD
* @createtime : 2020年06月13日 15:35
* @description : 订单表
*/
@Data
@TableName("`order`")
public class Order implements Serializable
{
private Long id;
private Long userId;
private String address;
}
新增包并命名为com.dgd.multidatasource.model.mybatis.mapper
,
新建UserMapper
类,代码如下:
package com.dgd.multidatasource.model.mybatis.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.dgd.multidatasource.model.mybatis.entity.User;
import org.apache.ibatis.annotations.Mapper;
/**
* @author : DaiGD
* @createtime : 2020年06月13日 15:42
* @description : 用户表映射接口
*/
@Mapper
public interface UserMapper extends BaseMapper
{
}
新建OrderMapper
类,代码如下:
package com.dgd.multidatasource.model.mybatis.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.dgd.multidatasource.model.mybatis.entity.Order;
import org.apache.ibatis.annotations.Mapper;
/**
* @author : DaiGD
* @createtime : 2020年06月13日 15:43
* @description : 订单表映射接口
*/
@Mapper
public interface OrderMapper extends BaseMapper
{
}
在配置类application.yml
上添加如下配置:
# DataSource Config
spring:
datasource:
# 指定驱动类
driver-class-name: com.mysql.jdbc.Driver
# 数据库地址
url: jdbc:mysql://localhost:3306/ds_01?serverTimezone=Asia/Shanghai&useSSL=false
# 数据库用户名
username: root
# 数据库用户密码
password: root
在MultiDatasourceApplication
类上指定Mapper
扫描路径,如下:
@MapperScan("com.dgd.multidatasource.model.mybatis.mapper")
写个单元测试来验证下MyBatis Plus
是否能正常访问ds_01
上的数据,代码如下:
package com.dgd.multidatasource.model.mybatis;
import com.dgd.multidatasource.model.mybatis.entity.User;
import com.dgd.multidatasource.model.mybatis.mapper.UserMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author : DaiGD
* @createtime : 2020年06月13日 15:39
* @description : MybatisPlus 功能测试
*/
@SpringBootTest
public class MybatisPlusTest
{
@Autowired
UserMapper userMapper;
@Test
void userTest()
{
User user = userMapper.selectById(2L);
Assertions.assertNotNull(user);
Assertions.assertEquals("小飞飞", user.getUserName(), "用户名不正确");
System.out.println("查询结果:" + user);
}
}
运行测试用例:
控制台输出如下结果,表明
Mybatis Plus
已能正常使用。
利用ShardingSphere实现多数据源切换
上面我们通过Mybatis Plus
已能正常访问ds_01
上的数据,但是如果想要同时访问ds_02
上的订单数据,就要借助ShardingSphere
中间件了,下面来引入相关依赖,如下:
指定版本号:
UTF-8
1.8
2.2.8.RELEASE
1.18.4
5.1.42
3.3.2
4.0.0-RC2
引入依赖:
org.apache.shardingsphere
sharding-jdbc-spring-boot-starter
${sharding-sphere.version}
接着我们把application.yml
文件里内容改成如下所示:
spring:
shardingsphere:
props:
sql:
show:
true
datasource:
names: ds1,ds2
ds1:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/ds_01?serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: root
ds2:
type: com.zaxxer.hikari.HikariDataSource
driverClassName: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/ds_02?serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: root
sharding:
defaultDatabaseStrategy:
hint:
algorithmClassName: com.dgd.multidatasource.shardingsphere.MyDatasourceRoutingAlgorithm
tables:
user:
actualDataNodes: ds1.user
order:
actualDataNodes: ds2.order
defaultTableStrategy:
none:
any: ""
我们对上面用到的参数做下说明:
spring:shardingsphere:props:sql:show
:是否开启SQL显示,默认是false
,开发过程我们把它设成true
以方便查看SQL执行过程。
spring:shardingsphere:datasource:names
:指定数据源名字,多个数据源之间以逗号分隔,下面就是对声明的数据源ds1
和ds2
进行相关属性配置,不再赘述。
spring:shardingsphere:sharding:defaultDatabaseStrategy:hint:algorithmClassName
:声明默认数据库分片策略使用Hint
策略,指定Hint
分片算法类名称,该类需实现HintShardingAlgorithm
接口并提供无参数的构造器。
spring:shardingsphere:sharding:tables
:数据分片规则配置,user
,order
是我们声明的逻辑表名称,actualDataNodes
指定实际的数据节点,由数据源名 + 逻辑表名组成,以小数点分隔。
spring:shardingsphere:sharding:defaultTableStrategy:none
:因为我们只是用到分库功能,并不需要进行分表,因此,指定默认的分表策略为none
,any
是我们给该策略取的名字,可以为任意字符串,其值为空。
更多参数配置项说明可参看官网。
从上面的配置内容可知,除了要配置数据源外,还有配置分片策略,由于我们希望的是想让它访问哪个数据源就访问哪个数据源,即强制路由,而ShardingSphere
的Hint
分片策略正好可以满足我们的这个需求。
以下关于Hint的简单介绍摘自官网。
ShardingSphere
使用ThreadLocal
管理分片键值进行Hint
强制路由。可以通过编程的方式向HintManager
中添加分片值,该分片值仅在当前线程内生效。
Hint
方式主要使用场景:
- 分片字段不存在SQL中、数据库表结构中,而存在于外部业务逻辑。
- 强制在主库进行某些数据操作。
更多分片策略可参考ShardingSphere官网。
下面我们来开始写分片策略的实现类,首先定义两个数据源常量,如下:
package com.dgd.multidatasource.shardingsphere;
/**
* @author : DaiGD
* @createtime : 2020年06月13日 16:46
* @description : 数据源枚举
*/
public enum DatasourceType
{
/**
* 用户数据源
*/
DATASOURCE_USER,
/**
* 订单数据源
*/
DATASOURCE_ORDER
}
数据库分片策略代码实现:
package com.dgd.multidatasource.shardingsphere;
import org.apache.shardingsphere.api.sharding.hint.HintShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.hint.HintShardingValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.HashSet;
/**
* @author : DaiGD
* @createtime : 2020年06月13日 16:42
* @description : 数据库分片策略
*/
public class MyDatasourceRoutingAlgorithm implements HintShardingAlgorithm
{
private static final Logger LOGGER = LoggerFactory.getLogger(MyDatasourceRoutingAlgorithm.class);
/**
* 用户数据源
*/
private static final String DS_USER = "ds1";
/**
* 订单数据源
*/
private static final String DS_ORDER = "ds2";
@Override
public Collection doSharding(Collection availableTargetNames, HintShardingValue shardingValue)
{
Collection result = new HashSet<>();
for(String value : shardingValue.getValues())
{
if(DatasourceType.DATASOURCE_USER.toString().equals(value))
{
if(availableTargetNames.contains(DS_USER))
{
result.add(DS_USER);
}
}
else
{
if(availableTargetNames.contains(DS_ORDER))
{
result.add(DS_ORDER);
}
}
}
LOGGER.info("availableTargetNames:{},shardingValue:{},返回的数据源:{}",
new Object[] { availableTargetNames, shardingValue, result });
return result;
}
}
好了,写个测试用例测试一下,新建包名为com.dgd.multidatasource.shardingsphere
,测试类名为DatasourceRoutingTest
,具体测试代码如下:
package com.dgd.multidatasource.shardingsphere;
import com.dgd.multidatasource.model.mybatis.entity.Order;
import com.dgd.multidatasource.model.mybatis.entity.User;
import com.dgd.multidatasource.model.mybatis.mapper.OrderMapper;
import com.dgd.multidatasource.model.mybatis.mapper.UserMapper;
import org.apache.shardingsphere.api.hint.HintManager;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author : DaiGD
* @createtime : 2020年06月13日 17:05
* @description : 数据源切换功能验证
*/
@SpringBootTest
public class DatasourceRoutingTest
{
@Autowired
UserMapper userMapper;
@Autowired
OrderMapper orderMapper;
@Test
void test()
{
HintManager hintManager = HintManager.getInstance();
// 分库不分表情况下,强制路由至某一个分库时,可使用hintManager.setDatabaseShardingValue方式添加分片
// 通过此方式添加分片键值后,将跳过SQL解析和改写阶段,从而提高整体执行效率。
// 详情参考:
// https://shardingsphere.apache.org/document/legacy/4.x/document/cn/manual/sharding-jdbc/usage/hint/
hintManager.setDatabaseShardingValue(DatasourceType.DATASOURCE_USER.toString());
// 访问用户数据源
User user = userMapper.selectById(2L);
Assertions.assertNotNull(user);
Assertions.assertEquals("小飞飞", user.getUserName(), "用户名不正确");
System.out.println("用户查询结果:" + user);
hintManager.close();
hintManager.setDatabaseShardingValue(DatasourceType.DATASOURCE_ORDER.toString());
// 访问订单数据源
Order order = orderMapper.selectById(1L);
Assertions.assertNotNull(order);
Assertions.assertEquals("北京市朝阳区", order.getAddress(), "地址不正确");
System.out.println("订单查询结果:" + order);
hintManager.close();
}
}
测试结果显示如下图所示,说明数据源已能成功切换:
最后,为了能在web端访问我们的项目,加上Controller
等相关代码,具体代码如下:
创建com.dgd.multidatasource.service
包,新建两个类,分别为UserService
,OrderService
,代码分别为:
package com.dgd.multidatasource.service;
import com.dgd.multidatasource.model.mybatis.entity.User;
import com.dgd.multidatasource.model.mybatis.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author : DaiGD
* @createtime : 2020年06月13日 19:14
* @description : 用户服务方法
*/
@Service
public class UserService
{
@Autowired
private UserMapper userMapper;
public User queryById(long id)
{
return userMapper.selectById(id);
}
}
package com.dgd.multidatasource.service;
import com.dgd.multidatasource.model.mybatis.entity.Order;
import com.dgd.multidatasource.model.mybatis.mapper.OrderMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author : DaiGD
* @createtime : 2020年06月13日 19:15
* @description : 订单服务方法
*/
@Service
public class OrderService
{
@Autowired
private OrderMapper orderMapper;
public Order queryById(long id)
{
return orderMapper.selectById(id);
}
}
在原来的controller
包下添加一个类,名为BusinessController
,代码如下:
package com.dgd.multidatasource.controller;
import com.dgd.multidatasource.model.mybatis.entity.Order;
import com.dgd.multidatasource.model.mybatis.entity.User;
import com.dgd.multidatasource.service.OrderService;
import com.dgd.multidatasource.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* @author : DaiGD
* @createtime : 2020年06月13日 19:17
* @description : 业务功能控制器
*/
@RestController
public class BusinessController
{
@Autowired
private UserService userService;
@Autowired
private OrderService orderService;
@GetMapping("/user/{id}")
public User queryByUserId(@PathVariable Long id)
{
return userService.queryById(id);
}
@GetMapping("/order/{id}")
public Order queryByOrderId(@PathVariable Long id)
{
return orderService.queryById(id);
}
}
之后启动项目,在浏览上分别输入:http://localhost:8080/user/1
和http://localhost:8080/order/2
,可以看到浏览器分别响应:
{"id":1,"userName":"Dannis"}
{"id":2,"userId":2,"address":"广州市海珠区"}
说明数据源切换在web层也正常。
防坑记录
-
对于分表策略,如果声明类型为
启动测试用例会提示如下异常:none
,如果不指定指定策略的名称和值,如下所示:
解决方法:
把any:""
的注释去掉即可。
参考。 因为我们的订单表名声明为了
order
,如果在Order
类上的@TableName
直接写成如下所示(注意,order
没有加上反引号):
@Data
@TableName("order")
public class Order implements Serializable
{
private Long id;
private Long userId;
private String address;
}
启动测试用例会提示如下异常:
order
当成了
MySql
内置关键字了,加上反引号区分开来即可,如下:
@Data
@TableName("`order`")
public class Order implements Serializable
{
private Long id;
private Long userId;
private String address;
}
项目完整代码地址
项目完整代码:
码云,GitHub。