IT必备技能spring

Java实战系列(1):SpringBoot+ShardingS

2020-06-15  本文已影响0人  在水之天

主要组件版本信息:

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方式主要使用场景:

更多分片策略可参考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层也正常。

防坑记录

@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

上一篇下一篇

猜你喜欢

热点阅读