主要組件版本信息:
SpringBoot:2.2.8.RELEASE
MyBatis Plus:3.3.2
ShardingSphere:4.0.0-RC2
需求說明
在企業(yè)開發(fā)中,如果業(yè)務數(shù)據(jù)分布在不同的數(shù)據(jù)源,那么我們就希望在訪問業(yè)務數(shù)據(jù)的時候,能夠根據(jù)業(yè)務需求,動態(tài)地切換數(shù)據(jù)源,ShardingSphere是一款不錯的數(shù)據(jù)庫中間件,利用它,可以很方便地實現(xiàn)我們想要的功能,下面,我們從零開始介紹,項目搭建及多數(shù)據(jù)源切換實現(xiàn)。
技術選型
Java 8 + MySql 5.7+ SpringBoot + Lombok + Mybatis Plus + ShardingSphere
開發(fā)工具:IntelliJ IDEA + Navicat
SpringBoot項目搭建
打開IDEA,新建一個SpringBoot 項目,如下圖示:
填寫完項目元數(shù)據(jù),點擊
Next繼續(xù)下一步,IDEA搭建SpringBoots項目非常方便。
項目創(chuàng)建完成后,我們來看下整體目錄結(jié)構,如下圖示:
我們調(diào)整下pom.xml,改成如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dgd</groupId>
<artifactId>multi-datasource</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>multi-datasource</name>
<description>多數(shù)據(jù)源切換</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<springboot.version>2.2.8.RELEASE</springboot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${springboot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<version>${springboot.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
spring-boot-starter是SpringBoot項目的核心,必須要引入;spring-boot-starter-web提供了web相關功能,而spring-boot-starter-test是SpringBoot的測試組件,后續(xù)我們寫單元測試會用到它。
下面我們來寫個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,該注解將幫助我們創(chuàng)建REST風格的web服務,具體講解參看此;寫一個方法名為:helloWorld,方法上添加注解GetMapping,表明該方法只接收GET請求,入?yún)⑸咸砑幼⒔?code>@PathVariable,它將幫我們讀取到請求路徑上定義的userName參數(shù)。此時我們的項目如下圖示:
接下來我們把項目啟動,回到MultiDatasourceApplication類,點擊綠色小圖標,選擇Run選項,啟動項目,如圖示:
看到控制臺輸出如下日志,表明項目啟動沒問題:
接著,我們在瀏覽器地址欄上輸入http://localhost:8080/hello/Dannis,看到網(wǎng)頁上出現(xiàn)Hello:Dannis,表明SpringBoot項目成功搭建完成。
數(shù)據(jù)初始化
現(xiàn)在我們來創(chuàng)建兩個數(shù)據(jù)源,真實場景的多數(shù)據(jù)源,數(shù)據(jù)庫所在的服務器一般是不相同的,如果是為了模擬真實環(huán)境,我們可以在自己電腦上搭建兩個虛擬機,分別搭建數(shù)據(jù)庫,或者利用Docker來創(chuàng)建兩個數(shù)據(jù)庫,或者買兩個云服務器,分別在上面搭建兩個數(shù)據(jù)庫,為了簡單起見,也可以是在同一個MySql服務上創(chuàng)建兩個不同的庫,我們就按最后一種情況來,假設已在本地上安裝好MySql服務環(huán)境,接下來,我們用下面的腳本命令來初始化我們的測試數(shù)據(jù):
# 創(chuàng)建第一個數(shù)據(jù)源
DROP DATABASE IF EXISTS `ds_01`;
CREATE DATABASE `ds_01`;
# 創(chuàng)建用戶表并初始化數(shù)據(jù)
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'),
('小飛飛');
# 創(chuàng)建第二個數(shù)據(jù)源
DROP DATABASE IF EXISTS `ds_02`;
CREATE DATABASE `ds_02`;
# 創(chuàng)建訂單表并初始化數(shù)據(jù)
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,'北京市朝陽區(qū)'),
(2,'廣州市海珠區(qū)');
SQL腳本執(zhí)行完畢,點擊localhost鼠標右鍵選擇刷新,然后可看到出現(xiàn)兩個數(shù)據(jù)庫ds_01和ds_02,打開查看一下,發(fā)現(xiàn)數(shù)據(jù)已正常寫入,如下圖所示:
利用Mybatis Plus來訪問數(shù)據(jù)
Mybatis Plus是ORM框架MyBatis的增強版,具體介紹可查看官網(wǎng)。
這里我們選用它來簡化對數(shù)據(jù)庫的操作,同時,我們也引入Lombok插件來簡化Java對象相關方法的編碼(IDEA需提前安裝好Lombok插件并添加相關配置,具體步驟可自行百度),在pom.xml添加如下代碼:
配置版本號:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<springboot.version>2.2.8.RELEASE</springboot.version>
<lombok.version>1.18.4</lombok.version>
<mysql-connector-java.version>5.1.42</mysql-connector-java.version>
<mybatis-plus.version>3.3.2</mybatis-plus.version>
</properties>
引入依賴:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector-java.version}</version>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- lombok 依賴 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
新增包并命名為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<User>
{
}
新建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<Order>
{
}
在配置類application.yml上添加如下配置:
# DataSource Config
spring:
datasource:
# 指定驅(qū)動類
driver-class-name: com.mysql.jdbc.Driver
# 數(shù)據(jù)庫地址
url: jdbc:mysql://localhost:3306/ds_01?serverTimezone=Asia/Shanghai&useSSL=false
# 數(shù)據(jù)庫用戶名
username: root
# 數(shù)據(jù)庫用戶密碼
password: root
在MultiDatasourceApplication類上指定Mapper掃描路徑,如下:
@MapperScan("com.dgd.multidatasource.model.mybatis.mapper")
寫個單元測試來驗證下MyBatis Plus是否能正常訪問ds_01上的數(shù)據(jù),代碼如下:
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("查詢結(jié)果:" + user);
}
}
運行測試用例:
控制臺輸出如下結(jié)果,表明
Mybatis Plus已能正常使用。利用ShardingSphere實現(xiàn)多數(shù)據(jù)源切換
上面我們通過Mybatis Plus已能正常訪問ds_01上的數(shù)據(jù),但是如果想要同時訪問ds_02上的訂單數(shù)據(jù),就要借助ShardingSphere中間件了,下面來引入相關依賴,如下:
指定版本號:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<springboot.version>2.2.8.RELEASE</springboot.version>
<lombok.version>1.18.4</lombok.version>
<mysql-connector-java.version>5.1.42</mysql-connector-java.version>
<mybatis-plus.version>3.3.2</mybatis-plus.version>
<sharding-sphere.version>4.0.0-RC2</sharding-sphere.version>
</properties>
引入依賴:
<!-- shardingSphere 依賴 -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>${sharding-sphere.version}</version>
</dependency>
接著我們把application.yml文件里內(nèi)容改成如下所示:
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: ""
我們對上面用到的參數(shù)做下說明:
spring:shardingsphere:props:sql:show:是否開啟SQL顯示,默認是false,開發(fā)過程我們把它設成true以方便查看SQL執(zhí)行過程。
spring:shardingsphere:datasource:names:指定數(shù)據(jù)源名字,多個數(shù)據(jù)源之間以逗號分隔,下面就是對聲明的數(shù)據(jù)源ds1和ds2進行相關屬性配置,不再贅述。
spring:shardingsphere:sharding:defaultDatabaseStrategy:hint:algorithmClassName:聲明默認數(shù)據(jù)庫分片策略使用Hint策略,指定Hint分片算法類名稱,該類需實現(xiàn)HintShardingAlgorithm接口并提供無參數(shù)的構造器。
spring:shardingsphere:sharding:tables:數(shù)據(jù)分片規(guī)則配置,user,order是我們聲明的邏輯表名稱,actualDataNodes指定實際的數(shù)據(jù)節(jié)點,由數(shù)據(jù)源名 + 邏輯表名組成,以小數(shù)點分隔。
spring:shardingsphere:sharding:defaultTableStrategy:none:因為我們只是用到分庫功能,并不需要進行分表,因此,指定默認的分表策略為none,any是我們給該策略取的名字,可以為任意字符串,其值為空。
更多參數(shù)配置項說明可參看官網(wǎng)。
從上面的配置內(nèi)容可知,除了要配置數(shù)據(jù)源外,還有配置分片策略,由于我們希望的是想讓它訪問哪個數(shù)據(jù)源就訪問哪個數(shù)據(jù)源,即強制路由,而ShardingSphere的Hint分片策略正好可以滿足我們的這個需求。
以下關于Hint的簡單介紹摘自官網(wǎng)。
ShardingSphere使用ThreadLocal管理分片鍵值進行Hint強制路由。可以通過編程的方式向HintManager中添加分片值,該分片值僅在當前線程內(nèi)生效。
Hint方式主要使用場景:
- 分片字段不存在SQL中、數(shù)據(jù)庫表結(jié)構中,而存在于外部業(yè)務邏輯。
- 強制在主庫進行某些數(shù)據(jù)操作。
更多分片策略可參考ShardingSphere官網(wǎng)。
下面我們來開始寫分片策略的實現(xiàn)類,首先定義兩個數(shù)據(jù)源常量,如下:
package com.dgd.multidatasource.shardingsphere;
/**
* @author : DaiGD
* @createtime : 2020年06月13日 16:46
* @description : 數(shù)據(jù)源枚舉
*/
public enum DatasourceType
{
/**
* 用戶數(shù)據(jù)源
*/
DATASOURCE_USER,
/**
* 訂單數(shù)據(jù)源
*/
DATASOURCE_ORDER
}
數(shù)據(jù)庫分片策略代碼實現(xiàn):
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 : 數(shù)據(jù)庫分片策略
*/
public class MyDatasourceRoutingAlgorithm implements HintShardingAlgorithm<String>
{
private static final Logger LOGGER = LoggerFactory.getLogger(MyDatasourceRoutingAlgorithm.class);
/**
* 用戶數(shù)據(jù)源
*/
private static final String DS_USER = "ds1";
/**
* 訂單數(shù)據(jù)源
*/
private static final String DS_ORDER = "ds2";
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, HintShardingValue<String> shardingValue)
{
Collection<String> 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:{},返回的數(shù)據(jù)源:{}",
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 : 數(shù)據(jù)源切換功能驗證
*/
@SpringBootTest
public class DatasourceRoutingTest
{
@Autowired
UserMapper userMapper;
@Autowired
OrderMapper orderMapper;
@Test
void test()
{
HintManager hintManager = HintManager.getInstance();
// 分庫不分表情況下,強制路由至某一個分庫時,可使用hintManager.setDatabaseShardingValue方式添加分片
// 通過此方式添加分片鍵值后,將跳過SQL解析和改寫階段,從而提高整體執(zhí)行效率。
// 詳情參考:
// https://shardingsphere.apache.org/document/legacy/4.x/document/cn/manual/sharding-jdbc/usage/hint/
hintManager.setDatabaseShardingValue(DatasourceType.DATASOURCE_USER.toString());
// 訪問用戶數(shù)據(jù)源
User user = userMapper.selectById(2L);
Assertions.assertNotNull(user);
Assertions.assertEquals("小飛飛", user.getUserName(), "用戶名不正確");
System.out.println("用戶查詢結(jié)果:" + user);
hintManager.close();
hintManager.setDatabaseShardingValue(DatasourceType.DATASOURCE_ORDER.toString());
// 訪問訂單數(shù)據(jù)源
Order order = orderMapper.selectById(1L);
Assertions.assertNotNull(order);
Assertions.assertEquals("北京市朝陽區(qū)", order.getAddress(), "地址不正確");
System.out.println("訂單查詢結(jié)果:" + order);
hintManager.close();
}
}
測試結(jié)果顯示如下圖所示,說明數(shù)據(jù)源已能成功切換:
最后,為了能在web端訪問我們的項目,加上Controller等相關代碼,具體代碼如下:
創(chuàng)建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 : 業(yè)務功能控制器
*/
@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":"廣州市海珠區(qū)"}
說明數(shù)據(jù)源切換在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內(nèi)置關鍵字了,加上反引號區(qū)分開來即可,如下:
@Data
@TableName("`order`")
public class Order implements Serializable
{
private Long id;
private Long userId;
private String address;
}