美文网首页IT必备技能spring
Java实战系列(1):SpringBoot+ShardingS

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

作者: 在水之天 | 来源:发表于2020-06-15 09:45 被阅读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方式主要使用场景:

    • 分片字段不存在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

    相关文章

      网友评论

        本文标题:Java实战系列(1):SpringBoot+ShardingS

        本文链接:https://www.haomeiwen.com/subject/tlccxktx.html