主要組件版本信息:
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
,改成如下所示:
<?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>多數據源切換</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的測試組件,後續我們寫單元測試會用到它。
下面我們來寫個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
添加如下代碼:
配置版本號:
<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:
# 指定驅動類
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
中間件了,下面來引入相關依賴,如下:
指定版本號:
<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
文件裏內容改成如下所示:
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<String>
{
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<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:{},返回的數據源:{}",
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;
}
啓動測試用例會提示如下異常:
顯然,SQL語句解析時出現了錯誤,它把我們的order
當成了MySql
內置關鍵字了,加上反引號區分開來即可,如下:
@Data
@TableName("`order`")
public class Order implements Serializable
{
private Long id;
private Long userId;
private String address;
}