SpringBoot 多数据源及事务解决方案 上

简介: SpringBoot 多数据源及事务解决方案 上


1. 背景

一个主库和N个应用库的数据源,并且会同时操作主库和应用库的数据,需要解决以下两个问题:

  • 如何动态管理多个数据源以及切换?
  • 如何保证多数据源场景下的数据一致性(事务)?

本文主要探讨这两个问题的解决方案,希望能对读者有一定的启发。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

2. 数据源切换原理

通过扩展Spring提供的抽象类AbstractRoutingDataSource,可以实现切换数据源。其类结构如下图所示:

  • targetDataSources&defaultTargetDataSource

项目上需要使用的所有数据源和默认数据源。

  • resolvedDataSources&resolvedDefaultDataSource

当Spring容器创建AbstractRoutingDataSource对象时,通过调用afterPropertiesSet复制上述目标数据源。由此可见,一旦数据源实例对象创建完毕,业务无法再添加新的数据源。

  • determineCurrentLookupKey

此方法为抽象方法,通过扩展这个方法来实现数据源的切换。目标数据源的结构为:Map<Object, DataSource>其key为lookup key

我们来看官方对这个方法的注释:

lookup key通常是绑定在线程上下文中,根据这个key去resolvedDataSources中取出DataSource。

根据目标数据源的管理方式不同,可以使用基于配置文件和数据库表两种方式。基于配置文件管理方案无法后续添加新的数据源,而基于数据库表方案管理,则更加灵活。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

3. 配置文件解决方案

根据上面的分析,我们可以按照下面的步骤去实现:

  • 定义DynamicDataSource类继承AbstractRoutingDataSource,重写determineCurrentLookupKey()方法。
  • 配置多个数据源注入targetDataSourcesdefaultTargetDataSource,通过afterPropertiesSet()方法将数据源写入resolvedDataSourcesresolvedDefaultDataSource
  • 调用AbstractRoutingDataSourcegetConnection()方法时,determineTargetDataSource()方法返回DataSource执行底层的getConnection()

其流程如下图所示:

3.1 创建数据源

DynamicDataSource数据源的注入,目前业界主流实现步骤如下:

在配置文件中定义数据源

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driverClassName=com.mysql.jdbc.Driver
# 主数据源
spring.datasource.druid.master.url=jdbcUrl
spring.datasource.druid.master.username=***
spring.datasource.druid.master.password=***
# 其他数据源
spring.datasource.druid.second.url=jdbcUrl
spring.datasource.druid.second.username=***
spring.datasource.druid.second.password=***

在代码中配置Bean

@Configuration
public class DynamicDataSourceConfig {
    @Bean
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource firstDataSource(){
        return DruidDataSourceBuilder.create().build();
    }
    @Bean
    @ConfigurationProperties("spring.datasource.druid.second")
    public DataSource secondDataSource(){
        return DruidDataSourceBuilder.create().build();
    }
    @Bean
    @Primary
    public DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>(5);
        targetDataSources.put(DataSourceNames.FIRST, firstDataSource);
        targetDataSources.put(DataSourceNames.SECOND, secondDataSource);
        return new DynamicDataSource(firstDataSource, targetDataSources);
    }
}

3.2 AOP处理

通过DataSourceAspect切面技术来简化业务上的使用,只需要在业务方法添加@SwitchDataSource注解即可完成动态切换:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface SwitchDataSource {
    String value();
}

DataSourceAspect拦截业务方法,更新当前线程上下文DataSourceContextHolder中存储的key,即可实现数据源切换。

3.3 方案不足

基于AbstractRoutingDataSource的多数据源动态切换,有个明显的缺点,无法动态添加和删除数据源。在我们的产品中,不能把应用数据源写死在配置文件。接下来分享一下基于数据库表的实现方案。

4. 数据库表解决方案

我们需要实现可视化的数据源管理,并实时查看数据源的运行状态。所以我们不能把数据源全部配置在文件中,应该将数据源定义保存到数据库表。参考AbstractRoutingDataSource的设计思路,实现自定义数据源管理。

4.1 设计数据源表

主库的数据源信息仍然配置在项目配置文件中,应用库数据源配置参数,则设计对应的数据表。表结构如下所示:

这个表主要就是DataSource的相关配置参数,其相应的ORM操作代码在此不再赘述,主要是实现数据源的增删改查操作。

4.2 自定义数据源管理

4.2.1 定义管理接口

通过继承AbstractDataSource即可实现DynamicDataSource。为了方便对数据源进行操作,我们定义一个接口DataSourceManager,为业务提供操作数据源的统一接口。

public interface DataSourceManager {
    void put(String var1, DataSource var2);
    DataSource get(String var1);
    Boolean hasDataSource(String var1);
    void remove(String var1);
    void closeDataSource(String var1);
    Collection<DataSource> all();
}

该接口主要是对数据表中定义的数据源,提供基础管理功能。

4.2.2 自定义数据源

DynamicDataSource的实现如下图所示:

根据前面的分析,AbstractRoutingDataSource是在容器启动的时候,执行afterPropertiesSet注入数据源对象,完成之后无法对数据源进行修改。DynamicDataSource则实现DataSourceManager接口,可以将数据表中的数据源加载到dataSources。

4.2.3 切面处理

这一块的处理跟配置文件数据源方案处理方式相同,都是通过AOP技术切换lookup key。

public DataSource determineTargetDataSource() {
        String lookupKey = DataSourceContextHolder.getKey();
        DataSource dataSource = Optional.ofNullable(lookupKey)
                .map(dataSources::get)
                .orElse(defaultDataSource);
        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine DataSource for lookup key [" + lookupKey + "]");
        }
        return dataSource;
    }

4.2.4 管理数据源状态

在项目启动的时候,加载数据表中的所有数据源,并执行初始化。初始化操作主要是使用SpringBoot提供的DataSourceBuilder类,根据数据源表的定义创建DataSource。在项目运行过程中,可以使用定时任务对数据源进行保活,为了提升性能再添加一层缓存。

AbstractRoutingDataSource 只支持单库事务,切换数据源是在开启事务之前执行。 Spring使用 DataSourceTransactionManager进行事务管理。开启事务,会将数据源缓存到DataSourceTransactionObject对象中,后续的commit和 rollback事务操作实际上是使用的同一个数据源。

如何解决切库事务问题?借助Spring的声明式事务处理,我们可以在多次切库操作时强制开启新的事务:

@SwitchDataSource    
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)

这样的话,执行切库操作的时候强制启动新事务,便可实现多次切库而且事务能够生效。但是这种事务方式,存在数据一致性问题:

假若ServiceB正常执行提交事务,接着返回ServiceA执行并且发生异常。因为两次处理是不同的事务,ServiceA这个事务执行回滚,而ServiceA事务已经提交。这样的话,数据就不一致了。接下来,我们主要讨论如何解决多库的事务问题。

相关文章
|
5月前
|
druid Java 关系型数据库
Spring Boot2 系列教程(二十五)Spring Boot 整合 Jpa 多数据源
Spring Boot2 系列教程(二十五)Spring Boot 整合 Jpa 多数据源
|
3月前
|
安全 Java API
SpringBoot + 事务钩子函数,打造高效支付系统!
【8月更文挑战第9天】在当今快速发展的数字支付时代,构建一个稳定、高效且安全的支付系统是企业数字化转型的关键一步。SpringBoot以其简洁的配置、快速的开发速度以及强大的生态支持,成为了构建支付系统的热门选择。而结合事务钩子函数(Transaction Hooks),则能进一步确保支付流程的完整性、一致性和可维护性。以下,我将分享如何利用SpringBoot与事务钩子函数来打造高效支付系统的技术实践。
83 15
SpringBoot + 事务钩子函数,打造高效支付系统!
|
3月前
|
Java
SpringBoot 配置多数据源
SpringBoot 配置多数据源
35 0
|
4月前
|
开发框架 Java 数据库
Spring Boot集成多数据源的最佳实践
Spring Boot集成多数据源的最佳实践
|
4月前
|
开发框架 Java 数据库
Spring Boot集成多数据源的最佳实践
Spring Boot集成多数据源的最佳实践
|
5月前
|
druid Java 关系型数据库
在Spring Boot中集成Druid实现多数据源有两种常用的方式:使用Spring Boot的自动配置和手动配置。
在Spring Boot中集成Druid实现多数据源有两种常用的方式:使用Spring Boot的自动配置和手动配置。
667 5
|
5月前
|
Java 数据管理 关系型数据库
Spring Boot中实现多数据源配置
Spring Boot中实现多数据源配置
|
5月前
|
关系型数据库 数据库 数据安全/隐私保护
springboot+dynamic-datasource多数据源配置动态切换
springboot+dynamic-datasource多数据源配置动态切换
568 0
|
6月前
|
存储 缓存 Java
SpringBoot 整合多数据源的事务问题
多数据源实现、切面的优先级问题(Order注解)、事务的传播特性(Propagation类)
|
5月前
|
Java 关系型数据库 MySQL
SpringBoot系列教程之事务传递属性
SpringBoot系列教程之事务传递属性