springboot中使用多数据源+动态切换数据源

对于springboot使用多数据源,有一个点就是对于数据源的切换是值得分析的。

数据源切换方式有几种?

对于同一个方法中使用不同数据源时配合spring的事务为何会切换数据源失败?那如何解决?如果解决了那会不会有分布式事务的问题(解决的方案可能是分不同事务去解决的,那不同事务就存在事务一致性(分布式事务的问题))

一、实现多数据源的方式

实现多数据源的方式主要有三种

1、不同包(实际不常用)

2、AOP(基于 Spring AbstractRoutingDataSource 做拓展)

3、数据库中间件(比较完美解决)

其中第二种和第三种较多人用

这里对于第二种情况进行分析,由于在第二种情况下结合spring事务会产生数据源切换不成功情况,此时则使用开源 baomidou 的一个项目dynamic-datasource-spring-boot-starter来实现多数据源的功能。

二、baomidou 多数据源(dynamic-datasource-spring-boot-starter

关于 dynamic-datasource-spring-boot-starter 的介绍,胖友自己看 官方文档 。😈 它和 MyBatis-Plus 都是开发者 baomidou 提供的。

1、pom文件:

 <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 实现对数据库连接池的自动化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency> <!-- 本示例,我们使用 MySQL -->
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.48</version>
        </dependency>

        <!-- 实现对 MyBatis 的自动化配置 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

        <!-- 实现对 dynamic-datasource 的自动化配置 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
            <version>2.5.7</version>
        </dependency>
        <!-- 不造为啥 dynamic-datasource-spring-boot-starter 会依赖这个 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-actuator</artifactId>
        </dependency>

        <!-- 方便等会写单元测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

yml (可以自己加上druid等连接池,这里为了简化容易看就不加了)

spring:
  datasource:
    # dynamic-datasource-spring-boot-starter 动态数据源的配置内容
    dynamic:
      primary: users # 设置默认的数据源或者数据源组,默认值即为 master
      datasource:
        # 订单 orders 数据源配置
        orders:
          url: jdbc:mysql://127.0.0.1:3306/test_orders?useSSL=false&useUnicode=true&characterEncoding=UTF-8
          driver-class-name: com.mysql.jdbc.Driver
          username: root
          password: 123456
        # 用户 users 数据源配置
        users:
          url: jdbc:mysql://127.0.0.1:3306/test_users?useSSL=false&useUnicode=true&characterEncoding=UTF-8
          driver-class-name: com.mysql.jdbc.Driver
          username: root
          password: 123456

# mybatis 配置内容
mybatis:
  #config-location: classpath:mybatis-config.xml # 配置 MyBatis 配置文件路径
  mapper-locations: classpath:mapper/*.xml # 配置 Mapper XML 地址
  type-aliases-package: com.datasource.entity # 配置数据库实体包路径
  configuration:
    map-underscore-to-camel-case: true  #开启驼峰
server:
  port: 9001

启动类:

@SpringBootApplication
@MapperScan(basePackages = "com.datasource.mapper")//扫描mapper接口
@EnableAspectJAutoProxy(exposeProxy = true)//配置 Spring AOP 能将当前代理对象设置到 AopContext 中,后面我们需要用AopContext拿到aop的代理对象
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

枚举类:
 

//枚举类
public class DBConstants {

    /**
     * 数据源分组 - 订单库
     */
    public static final String DATASOURCE_ORDERS = "orders";

    /**
     * 数据源分组 - 用户库
     */
    public static final String DATASOURCE_USERS = "users";

}

新建order库和order表

新建user库和表

实体类:

order

@Data
public class Order implements Serializable {

    private int id;
    private int userId;
}

user

@Data
public class User implements Serializable {


    private int id;
    private String username;
    private String password;
    private Date createTime;

}

mapper接口:

//order的mapper接口
@Repository
@DS(DBConstants.DATASOURCE_ORDERS)
public interface OrderMapper {

    Order selectById(@Param("id") Integer id);

}

//user的mapper接口
@Repository
@DS(DBConstants.DATASOURCE_USERS)//dynamic-datasource-spring-boot-starter,数据源名
public interface UserMapper {

    User selectById(@Param("id") Integer id);

}

xml:
 

//order的xml文件
<mapper namespace="com.datasource.mapper.OrderMapper">

    <sql id="FIELDS">
        id, user_id
    </sql>

    <select id="selectById" parameterType="integer" resultType="com.datasource.entity.Order">
        SELECT
        <include refid="FIELDS" />
        FROM orders
        WHERE id = #{id}
    </select>

</mapper>

//user的xml文件
<mapper namespace="com.datasource.mapper.UserMapper">

    <sql id="FIELDS">
        id, username,password,create_time
    </sql>

    <select id="selectById" parameterType="integer" resultType="com.datasource.entity.User">
        SELECT
        <include refid="FIELDS" />
        FROM users
        WHERE id = #{id}
    </select>

</mapper>

service层(这里设置了5个场景,等下分别来分析各个场景的问题)

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private UserMapper userMapper;


    //获取到aop的代理对象(如果用this调用此时拿到的不是aop的代理对象)
    private OrderService self() {
        return (OrderService) AopContext.currentProxy();
    }

//-------------------------场景1----------------------------
//未开启事务,则可以自己使用自己的数据源,并不会报异常
    public void method01() {
        // 查询订单
        Order order = orderMapper.selectById(1);
        System.out.println(order);
        // 查询用户
        User user = userMapper.selectById(1);
        System.out.println(user);
    }

//-------------------------场景2----------------------------

    //此时开启事务(经过aop代理),会抛出找不到表的情况,因为此时事务开启就会默认拿到默认的数据源user,
    // 而第一次查询是要使用order数据源的,此时因为spring事务则无法从默认的user切换到order数据源上
    //这里就是结合spring事务出现的数据源切换不成功的情况(具体原因后面分析)
    @Transactional
    public void method02() {
        // 查询订单
        Order order = orderMapper.selectById(1);
        System.out.println(order);
        // 查询用户
        User user = userMapper.selectById(1);
        System.out.println(user);
    }

//-------------------------场景3----------------------------

    public void method03() {
        // 查询订单
        self().method031();
        // 查询用户
        self().method032();
    }

    @Transactional // 报错,因为此时获取的是 primary 对应的 DataSource ,即 users 。
    public void method031() {
        Order order = orderMapper.selectById(1);
        System.out.println(order);
    }

    @Transactional
    public void method032() {
        User user = userMapper.selectById(1);
        System.out.println(user);
    }

//-------------------------场景4----------------------------

    //未报异常,正常结束(两个方法各自执行各自的事务,并且根据@DS获取到各自的数据源)
    public void method04() {
        // 查询订单
        self().method041();
        // 查询用户
        self().method042();
    }

    @Transactional
    @DS(DBConstants.DATASOURCE_ORDERS)
    public void method041() {
        Order order = orderMapper.selectById(1);
        System.out.println(order);
    }

    @Transactional
    @DS(DBConstants.DATASOURCE_USERS)
    public void method042() {
        User user = userMapper.selectById(1);
        System.out.println(user);
    }

//-------------------------场景5----------------------------
    //正常执行,05方法中调用052方法,在一个事务中调用另一个事务方法,
    // 另一个事务方法采用,重新建立一个事务的方法并使用自己的数据源
    // 此时在05方法中则会出现两个事务,这时就涉及到分布式事务

    @Transactional
    @DS(DBConstants.DATASOURCE_ORDERS)//声明自己要用到的数据源,这样就不会去拿默认数据源了
    public void method05() {
        // 查询订单
        Order order = orderMapper.selectById(1);
        System.out.println(order);
        // 查询用户
        self().method052();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    @DS(DBConstants.DATASOURCE_USERS)
    public void method052() {
        User user = userMapper.selectById(1);
        System.out.println(user);
    }

 

}

此时用controller的接口来测试各个场景

@RestController
public class DatasourceController {

    @Autowired
    OrderService orderService;

    //场景1
    @GetMapping("/method1")
    public void getMethod1(){

        orderService.method01();
    }

    //场景2
    @GetMapping("/method2")
    public void getMethod2(){

        orderService.method02();
    }
    //场景3
    @GetMapping("/method3")
    public void getMethod3(){

        orderService.method03();
    }
    //场景4
    @GetMapping("/method4")
    public void getMethod4(){

        orderService.method04();
    }
    //场景5
    @GetMapping("/method5")
    public void getMethod5(){

        orderService.method05();
    }



}

下面针对各个场景进行分析

1、场景1

此时场景1未使用spring的事务,即没开启事务,访问场景1接口http://localhost:9001/method1

此时运行正常,控制台打印出正常信息

原因:

  • 方法未使用 @Transactional 注解,不会开启事务。
  • 对于 OrderMapper 和 UserMapper 的查询操作,分别使用其接口上的 @DS 注解,找到对应的数据源,执行操作。
  • 这样一看,在未开启事务的情况下,我们已经能够自由的使用多数据源进行切换。
 public void method01() {
        // 查询订单
        Order order = orderMapper.selectById(1);
        System.out.println(order);
        // 查询用户
        User user = userMapper.selectById(1);
        System.out.println(user);
    }

2、场景2


    @Transactional
    public void method02() {
        // 查询订单
        Order order = orderMapper.selectById(1);
        System.out.println(order);
        // 查询用户
        User user = userMapper.selectById(1);
        System.out.println(user);
    }

此时报错:Table 'test_users.orders' doesn't exist  (即在tset_users数据库中找不到orders表)

为什么?我们不是在mapper接口各自的使用的各自的数据源?

注意,此时的错误发生在orderMapper查询该行代码上,而此时从错误中我们可以看出他是在user库中找order表?

那不就是说我们在执行orderMapper接口查询时拿到的是user的数据源信息(所以才会去user库)

那么这是为什么呢?我们不是在 OrderMapper 上,声明使用 orders 数据源了么?结果为什么会使用 users 数据库,路由到 test_users 库上呢。

原因如下:

  • 这里,就和 Spring 事务的实现机制有关系。因为方法添加了 @Transactional 注解,Spring 事务就会生效。此时,Spring TransactionInterceptor 会通过 AOP 拦截该方法,创建事务。而创建事务,势必就会获得数据源。那么,TransactionInterceptor 会使用 Spring DataSourceTransactionManager 创建事务,并将事务信息通过 ThreadLocal 绑定在当前线程。
  • 而事务信息,就包括事务对应的 Connection 连接。那也就意味着,还没走到 OrderMapper 的查询操作,Connection 就已经被创建出来了。并且,因为事务信息会和当前线程绑定在一起,在 OrderMapper 在查询操作需要获得 Connection 时,就直接拿到当前线程绑定的 Connection ,而不是 OrderMapper 添加 @DS 注解所对应的 DataSource 所对应的 Connection 。
  • OK ,那么我们现在可以把问题聚焦到 DataSourceTransactionManager 是怎么获取 DataSource 从而获得 Connection 的了。对于每个 DataSourceTransactionManager 数据库事务管理器,创建时都会传入其需要管理的 DataSource 数据源。在使用 dynamic-datasource-spring-boot-starter 时,它创建了一个 DynamicRoutingDataSource ,传入到 DataSourceTransactionManager 中。
  • 而 DynamicRoutingDataSource 负责管理我们配置的多个数据源。例如说,本示例中就管理了 ordersusers 两个数据源,并且默认使用 users 数据源。那么在当前场景下,DynamicRoutingDataSource 需要基于 @DS 获得数据源名,从而获得对应的 DataSource ,结果因为我们在 Service 方法上,并没有添加 @DS 注解,所以它只好返回默认数据源,也就是 users 。故此,就发生了 Table 'test_users.orders' doesn't exist 的异常。

也就是说此时要切换数据源,需要在service层(即 @Transactional的方法上设置是哪个数据源,这样DynamicRoutingDataSource才会去拿到相应的数据源)设置使用哪个数据源,如果不设置此时DynamicRoutingDataSource会去拿到我们默认的数据源(即user),而后面的场景4就是根据这样实现的。

(那么这里提出一个疑问:mapper接口上设置的数据源不就不起作用?那是为什么呢?这个问题需要我再去学学然后再回来解释)(亲测在场景4中使用了在service设置数据源后删掉mapper接口的数据源设置后没有影响结果)

3、场景3

public void method03() {
    // 查询订单
    self().method031();
    // 查询用户
    self().method032();
}

@Transactional // 报错,因为此时获取的是 primary 对应的 DataSource ,即 users 。
public void method031() {
    OrderDO order = orderMapper.selectById(1);
    System.out.println(order);
}

@Transactional
public void method032() {
    UserDO user = userMapper.selectById(1);
    System.out.println(user);
}

抛出异常:Table 'test_users.orders' doesn't exist

和场景2等价的。

其中self()是获取到aop的代理对象,如果此时,我们将 #self() 代码替换成 this 之后,诶,结果就正常执行。这又是为什么呢?其实,这样调整后,因为 this 不是代理对象,所以 #method031() 和 #method032() 方法上的 @Transactional 直接没有作用,Spring 事务根本没有生效。所以,最终结果和场景一是等价的。

4、场景4

public void method04() {
    // 查询订单
    self().method041();
    // 查询用户
    self().method042();
}

@Transactional
@DS(DBConstants.DATASOURCE_ORDERS)
public void method041() {
    OrderDO order = orderMapper.selectById(1);
    System.out.println(order);
}

@Transactional
@DS(DBConstants.DATASOURCE_USERS)
public void method042() {
    UserDO user = userMapper.selectById(1);
    System.out.println(user);
}

正常执行,原因:

  • 和 @method03() 方法,差异在于,#method041() 和 #method042() 方法上,添加 @DS 注解,声明对应使用的 DataSource 。
  • 执行方法,正常结束,未抛出异常。是不是觉得有点奇怪?
  • 在执行 #method041() 方法前,因为有 @Transactional 注解,所以 Spring 事务机制触发。DynamicRoutingDataSource 根据 @DS 注解,获得对应的 orders 的 DataSource ,从而获得 Connection 。所以后续 OrderMapper 执行查询操作时,即使使用的是线程绑定的 Connection ,也可能不会报错。😈 嘿嘿,实际上,此时 OrderMapper 上的 @DS 注解,也没有作用。
  • 对于 #method042() ,也是同理。但是,我们上面不是提了 Connection 会绑定在当前线程么?那么,在 #method042() 方法中,应该使用的是 #method041() 的 orders 对应的 Connection 呀。在 Spring 事务机制中,在一个事务执行完成后,会将事务信息和当前线程解绑。所以,在执行 #method042() 方法前,又可以执行一轮事务的逻辑。
  • 【重要】总的来说,对于声明了 @Transactional 的 Service 方法上,也同时通过 @DS 声明对应的数据源。

       这时就解决了场景2、3的数据源切换不成功的问题了,即在各个@Transactional设置不同的数据源,但这里要明白,设置不同的数据源,此时场景4的method4方法是启动了两个事务,则此时就会有分布式事务问题(即可能事务1执行完成,事务2回滚了,而method4实际要达到的目的是事务1、2要么都完成,要么都回滚,所以这里的分布式事务需要进一步解决)

5、场景5

@Transactional
@DS(DBConstants.DATASOURCE_ORDERS)
public void method05() {
    // 查询订单
    OrderDO order = orderMapper.selectById(1);
    System.out.println(order);
    // 查询用户
    self().method052();
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
@DS(DBConstants.DATASOURCE_USERS)
public void method052() {
    UserDO user = userMapper.selectById(1);
    System.out.println(user);
}

在一个事务中重开一个事务,这时也解决了事务中切换不成功的情况

执行正常,原因:

  • 和 @method04() 方法,差异在于,我们直接在 #method05() 方法中,此时处于一个事务中,直接调用了 #method052() 方法。
  • 执行方法,正常结束,未抛出异常。是不是觉得有点奇怪?
  • 我们仔细看看 #method052() 方法,我们添加的 @Transactionl 注解,使用的事务传播级别是 Propagation.REQUIRES_NEW 。此时,在执行 #method052() 方法之前,TransactionInterceptor 会将原事务挂起,暂时性的将原事务信息和当前线程解绑。
    • 所以,在执行 #method052() 方法前,又可以执行一轮事务的逻辑。
    • 之后,在执行 #method052() 方法完成后,会将原事务恢复,重新将原事务信息和当前线程绑定。

注意:这里由于开启了两个事务,也会存在分布式事务问题。

而这个场景则说明如何在一个事务方法中切换数据源

上面是对数据源源切换使用做了介绍,而多数据源也涉及了读写分离、分库分表的实现,虽然上面这种方案也可以实现,但是每次都要去配置一个数据源和其他配置信息1,这会非常的麻烦,那么下面会介绍一款开源的数据库中间件来解决这些事情

我在看见网上还有其他的解决方案:大概是不使用spring的声明式事务,自己提交回滚事务,从而达到切换数据源成功(即编程式事务)https://www.cnblogs.com/cq-yangzhou/p/10945779.html

https://blog.csdn.net/u010928589/article/details/91348761

https://blog.csdn.net/qq1010830256/article/details/106652663

(这个方法也值得我分析实现)

sharedingsphere

上面对于基于 Spring AbstractRoutingDataSource 做拓展(aop)的方案结合开源包进行了分析,虽然上面可以进行数据源切换,但是其解决方法中开启了多个事务,此时则会面临分布式事务的问题,现在对于分布式事务的解决方案,主要有下面几个方案:
刚性事务/低并发:2/3pc

柔性事务/长事务/高并发:tcc、saga、mq等

那么我们选择什么呢?

此时我们就需要选择一个阿帕奇的开源项目sharedingsphere,其中包含shareding jdbc(client模式),sharding proxy(proxy代理模式)、和正在开发的shareding sidecar。

我们可以使用其中的shareding jdbc或者sharding proxy,这里我们主要选择前者,其一少了一层代理层性能更高,但是配置可能会麻烦一点(现在比较多的大厂还是在用client模式的)。而且对于分布式事务,sharedingsphere也支持刚性柔性事务(seata等框架),可以完美的解决多数据源、分库分表、读写分离、分布式事务的解决方案。

其实还有一款比较多人使用的数据库中间件mycat(不太适合高并发)(client模式)

下面有关一个分库分表技术说明文章:https://www.bilibili.com/read/cv7536093?spm_id_from=333.788.b_636f6d6d656e74.23

对于sharedingsphere我不打算在这篇文章讲,后面会写关于怎么使用sharedingsphere来实现多数据源切换、分库分表、读写分离、分布式事务问题的实现。

本文主要参考这位大哥的文章:http://www.iocoder.cn/Spring-Boot/dynamic-datasource/#

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值