Java實戰系列(1):SpringBoot+ShardingSphere實現多數據源切換

主要組件版本信息:

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 項目,如下圖示:

創建項目

創建項目2填寫項目元數據
填寫完項目元數據,點擊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參數。此時我們的項目如下圖示:

Hello World

接下來我們把項目啓動,回到MultiDatasourceApplication類,點擊綠色小圖標,選擇Run選項,啓動項目,如圖示:
啓動項目1
啓動項目2

看到控制檯輸出如下日誌,表明項目啓動沒問題:

啓動項目3

接着,我們在瀏覽器地址欄上輸入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_01ds_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:指定數據源名字,多個數據源之間以逗號分隔,下面就是對聲明的數據源ds1ds2進行相關屬性配置,不再贅述。
spring:shardingsphere:sharding:defaultDatabaseStrategy:hint:algorithmClassName:聲明默認數據庫分片策略使用Hint策略,指定Hint分片算法類名稱,該類需實現HintShardingAlgorithm接口並提供無參數的構造器。
spring:shardingsphere:sharding:tables:數據分片規則配置,userorder是我們聲明的邏輯表名稱,actualDataNodes指定實際的數據節點,由數據源名 + 邏輯表名組成,以小數點分隔。
spring:shardingsphere:sharding:defaultTableStrategy:none:因爲我們只是用到分庫功能,並不需要進行分表,因此,指定默認的分表策略爲noneany是我們給該策略取的名字,可以爲任意字符串,其值爲空。

更多參數配置項說明可參看官網

從上面的配置內容可知,除了要配置數據源外,還有配置分片策略,由於我們希望的是想讓它訪問哪個數據源就訪問哪個數據源,即強制路由,而ShardingSphereHint分片策略正好可以滿足我們的這個需求。

以下關於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包,新建兩個類,分別爲UserServiceOrderService,代碼分別爲:

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/1http://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;
}

項目完整代碼地址

項目完整代碼:
碼雲GitHub

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章