Spring Boot中实现多数据源动态切换效果(2):通过开源项目Dynamic Datasource Spring Boot Starter实现

3,532 阅读6分钟

目录:

在Spring Boot中,可以通过多种方式实现多数据源的动态切换效果,在本篇文章中我介绍第二种实现方案。

一 具体实现

(1)测试使用的数据库

这里我们创建3个数据库,分别是:db01db02db03,然后这3个数据库都有一张名为user_info的表,表结构一样,只是数据不同。

-- 建表语句
DROP TABLE IF EXISTS `user_info`;
CREATE TABLE `user_info` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `name` varchar(255) DEFAULT NULL COMMENT '姓名',
  `age` int(11) DEFAULT NULL COMMENT '年龄',
  `addr_city` varchar(255) DEFAULT NULL COMMENT '所在城市',
  `addr_district` varchar(255) DEFAULT NULL COMMENT '所在区',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- db01中表「user_info」的数据
INSERT INTO `user_info` VALUES ('1', '张三', '20', '北京', '朝阳区');
INSERT INTO `user_info` VALUES ('2', '李四', '18', '北京', '东城区');

-- db02中表「user_info」的数据
INSERT INTO `user_info` VALUES ('1', '王五', '22', '上海', '普陀区');
INSERT INTO `user_info` VALUES ('2', '赵六', '24', '上海', '浦东新区');

-- db03中表「user_info」的数据
INSERT INTO `user_info` VALUES ('1', '孙七', '28', '成都', '武侯区');
INSERT INTO `user_info` VALUES ('2', '周八', '26', '成都', '天府新区');

(2)在pom.xml文件中添加相关依赖

<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
	<version>3.2.1</version>
</dependency>

最新版本:mvnrepository.com/artifact/co…

(3)新增application-datasource2.yml配置文件

新建这个用于测试的配置文件,主要配置了接下来需要用到的多个数据源,其配置如下:

server:
  port: 8080
  servlet:
    session.timeout: 300

logging:
  level:
    org.springframework.web: debug
    cn.zifangsky: debug
  file:
    name: web-exercise.log
    path: logs

spring:
  datasource:
    # HikariCP 连接池配置
    hikari:
      pool-name: exercise_HikariCP
      minimum-idle: 5  #最小空闲连接数量
      idle-timeout: 30000  #空闲连接存活最大时间,默认600000(10分钟)
      maximum-pool-size: 20  #连接池最大连接数,默认是10
      auto-commit: true  #此属性控制从池返回的连接的默认自动提交行为,默认值:true
      max-lifetime: 1800000  #此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认1800000即30分钟
      connection-timeout: 30000  #数据库连接超时时间,默认30秒,即30000
    dynamic:
      primary: db01  #设置默认的数据源或者数据源组,默认值为master
      datasource:
        db01:
          type: com.zaxxer.hikari.HikariDataSource
          driver-class-name: com.mysql.jdbc.Driver
          url: jdbc:mysql://localhost:3306/db01?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&failOverReadOnly=false&useSSL=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
          username: root
          password: root
        db02:
          type: com.zaxxer.hikari.HikariDataSource
          driver-class-name: com.mysql.jdbc.Driver
          url: jdbc:mysql://localhost:3306/db02?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&failOverReadOnly=false&useSSL=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
          username: root
          password: root
        db03:
          type: com.zaxxer.hikari.HikariDataSource
          driver-class-name: com.mysql.jdbc.Driver
          url: jdbc:mysql://localhost:3306/db03?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&failOverReadOnly=false&useSSL=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
          username: root
          password: root

#mybatis
mybatis:
  type-aliases-package: cn.zifangsky.example.webexercise.mapper
  mapper-locations: classpath:mapper/*.xml

(4)新建一个测试使用的Mapper

上篇文章的基础上,再新建一个测试使用的Mapper,跟上篇文章的那个Mapper类似,只是使用的注解不同而已。

package cn.zifangsky.example.webexercise.mapper;

import cn.zifangsky.example.webexercise.model.UserInfo;
import com.baomidou.dynamic.datasource.annotation.DS;
import org.apache.ibatis.annotations.Param;

@DS("db02")
public interface UserInfoDynamicMapper2 {
    /**
     * 通过默认数据源查询,方法级别的注解优先级更高
     */
    @DS("db01")
    UserInfo selectByDefaultDataSource(Integer id);

    /**
     * 方法级别没有添加注解,则使用接口级别的注解,通过 db02 数据源查询
     */
    UserInfo selectByDB02DataSource(Integer id);

    /**
     * 通过 db03 数据源查询
     */
    @DS("db03")
    UserInfo selectByDB03DataSource(Integer id);

    /**
     * 测试事务是否回滚(数据插入 db02 数据源)
     */
    @DS("db02")
    int addToDB02(UserInfo record);

    /**
     * 测试事务是否回滚(数据插入 db03 数据源)
     */
    @DS("db03")
    int addToDB03(UserInfo record);

    /**
     * 从 db03 数据源删除数据
     */
    @DS("db03")
    int deleteFromDB03ByName(@Param("name") String name);
}

其对应的UserInfoDynamicMapper.xml文件(文件内容除了类路径不同,其他跟上篇文章的那个Mapper.xml一样)是:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.zifangsky.example.webexercise.mapper.UserInfoDynamicMapper2">
  <resultMap id="BaseResultMap" type="cn.zifangsky.example.webexercise.model.UserInfo">
    <id column="id" jdbcType="INTEGER" property="id" />
    <result column="name" jdbcType="VARCHAR" property="name" />
    <result column="age" jdbcType="INTEGER" property="age" />
    <result column="addr_city" jdbcType="VARCHAR" property="addrCity" />
    <result column="addr_district" jdbcType="VARCHAR" property="addrDistrict" />
  </resultMap>
  <sql id="Base_Column_List">
    id, `name`, age, addr_city, addr_district
  </sql>
  <select id="selectByDefaultDataSource" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from user_info
    where id = #{id,jdbcType=INTEGER}
  </select>

  <select id="selectByDB02DataSource" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from user_info
    where id = #{id,jdbcType=INTEGER}
  </select>

  <select id="selectByDB03DataSource" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from user_info
    where id = #{id,jdbcType=INTEGER}
  </select>

  <insert id="addToDB02" keyColumn="id" keyProperty="id" parameterType="cn.zifangsky.example.webexercise.model.UserInfo" useGeneratedKeys="true">
    insert into user_info (`name`, age, addr_city,
      addr_district)
    values (#{name,jdbcType=VARCHAR}, #{age,jdbcType=INTEGER}, #{addrCity,jdbcType=VARCHAR},
      #{addrDistrict,jdbcType=VARCHAR})
  </insert>
  <insert id="addToDB03" keyColumn="id" keyProperty="id" parameterType="cn.zifangsky.example.webexercise.model.UserInfo" useGeneratedKeys="true">
    insert into user_info (`name`, age, addr_city,
      addr_district)
    values (#{name,jdbcType=VARCHAR}, #{age,jdbcType=INTEGER}, #{addrCity,jdbcType=VARCHAR},
      #{addrDistrict,jdbcType=VARCHAR})
  </insert>

  <delete id="deleteFromDB03ByName" parameterType="java.lang.String">
    delete from user_info
    where name = #{name,jdbcType=VARCHAR}
  </delete>
</mapper>

(5)使用单元测试测试「动态切换数据源」的效果

package cn.zifangsky.example.webexercise.dataSource;

import cn.zifangsky.example.webexercise.mapper.UserInfoDynamicMapper2;
import cn.zifangsky.example.webexercise.mapper.UserInfoMapper;
import cn.zifangsky.example.webexercise.model.UserInfo;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;

import java.sql.SQLException;

/**
 * 测试动态切换数据源(Dynamic Datasource Spring Boot Starter)
 *
 * @author zifangsky
 * @date 2020/11/6
 * @since 1.0.0
 */
@DisplayName("测试动态切换数据源(Dynamic Datasource Spring Boot Starter)")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class DynamicDataSource2Test {

    @Autowired
    private UserInfoMapper userInfoMapper;

    @Autowired
    private UserInfoDynamicMapper2 userInfoDynamicMapper2;

    @Test
    @Order(1)
    @DisplayName("普通方法——使用默认数据源")
    public void testCommonMethod(){
        UserInfo userInfo = userInfoMapper.selectByPrimaryKey(1);

        Assertions.assertNotNull(userInfo);
        Assertions.assertEquals("张三", userInfo.getName());
    }

    @Test
    @Order(2)
    @DisplayName("通过默认数据源查询,方法级别的注解优先级更高")
    public void testSelectByDefaultDataSource(){
        UserInfo userInfo = userInfoDynamicMapper2.selectByDefaultDataSource(1);

        Assertions.assertNotNull(userInfo);
        Assertions.assertEquals("张三", userInfo.getName());
    }

    @Test
    @Order(3)
    @DisplayName("方法级别没有添加注解,则使用接口级别的注解,通过 db02 数据源查询")
    public void testSelectByDB02DataSource(){
        UserInfo userInfo = userInfoDynamicMapper2.selectByDB02DataSource(1);

        Assertions.assertNotNull(userInfo);
        Assertions.assertEquals("王五", userInfo.getName());
    }

    @Test
    @Order(4)
    @DisplayName("方法级别添加注解,手动指定通过 db03 数据源查询")
    public void testSelectByDB03DataSource(){
        UserInfo userInfo = userInfoDynamicMapper2.selectByDB03DataSource(1);

        Assertions.assertNotNull(userInfo);
        Assertions.assertEquals("孙七", userInfo.getName());
    }

    @Test
    @Order(5)
    @DisplayName("在一个方法执行过程中嵌套操作多个数据源的情况")
    public void testNestedMultiDataSource(){
        //1. 从 db02 查询一条数据
        UserInfo userInfo = userInfoDynamicMapper2.selectByDB02DataSource(1);

        //2. 插入到 db03
        userInfo.setId(null);
        userInfoDynamicMapper2.addToDB03(userInfo);
    }

    @Test
    @Order(6)
    @DisplayName("从 db03 数据源删除数据")
    public void testDeleteFromDB03ByName(){
        userInfoDynamicMapper2.deleteFromDB03ByName("王五");
    }

    @Test
    @Order(7)
    @DisplayName("嵌套多个数据源的事务回滚情况")
    @Transactional(rollbackFor = Exception.class)
    public void testTransaction() throws SQLException {
        //1. 从 db01 查询一条数据
        UserInfo userInfo = userInfoDynamicMapper2.selectByDefaultDataSource(1);

        //2. 分别插入到 db02 和 db03
        userInfo.setId(null);
        userInfoDynamicMapper2.addToDB02(userInfo);
        userInfoDynamicMapper2.addToDB03(userInfo);

        //3. 手动抛出一个异常,测试事务回滚效果
        throw new SQLException("SQL执行过程中发生某些未知异常");
    }

}

注:以上测试代码基于Junit5 测试框架编写,需要的依赖如下:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
	<exclusions>
		<exclusion>
			<groupId>org.junit.vintage</groupId>
			<artifactId>junit-vintage-engine</artifactId>
		</exclusion>
	</exclusions>
</dependency>

运行单元测试后,其测试结果跟上篇文章一样,这里就省略截图吧。

二 这两种方案如何选择

我看了一下开源项目Dynamic Datasource Spring Boot Starter的源代码,发现它也有一个DynamicRoutingDataSource(com/baomidou/dynamic/datasource/DynamicRoutingDataSource.java),然后具体的实现逻辑跟我在上篇文章中介绍的那种方案实际也是类似的。

com/baomidou/dynamic/datasource/DynamicRoutingDataSource类

不过,通过查看这个开源项目的官方文档可以得知,这个项目支持的特性比较丰富,截止目前有以下这些:

然后,经过了多次更新迭代后,这个开源项目也相对比较稳定。因此,在这里我给出的建议是:

  • 如果想要实现简单,或者说想要将以上截图中的部分特性拿来就用,那么可以考虑使用这个开源项目;
  • 如果想要实现的功能比较单一,而且有尽可能减少外部依赖的需求,那么通过上篇文章介绍的方案来手动实现也是可以的。

参考: