likes
comments
collection
share

【SpringBoot】实现动态数据源切换本文主要记录了如何在SpringBoot中实现动态数据源切换,主要步骤如下:

作者站长头像
站长
· 阅读数 61

本文主要记录了如何在SpringBoot中实现动态数据源切换,主要步骤如下:

  • 从配置文件中读取主从数据源配置
  • 加载配置信息到IOC容器中
  • 使用动态数据源切换[新增操作]
  • 使用注解+AOP优化使用过程

前置条件:

  • 开发环境:jdk8+MySQL8
  • 已成功搭建 SpringBoot + MyBatis 应用。

建表的SQL语句如下:

-- auto-generated definition
drop table  user;
create table user
(
    user_id   int auto_increment comment '主键' primary key,
    user_name varchar(20)  not null comment '用户名',
    password  varchar(255) null
)ENGINE=InnoDB AUTO_INCREMENT=11600036 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci

在配置文件中配置数据源信息

首先在配置文件中配置相关数据源的信息,这里为一主库二从库:

server:
  port: 8888
spring:
    master:
      jdbc-url: jdbc:mysql://localhost:3306/master?useUnicode=true&characterEncoding=utf8
      username: root
      password: root123!
      driver-class-name: com.mysql.cj.jdbc.Driver
    slave1:
      jdbc-url: jdbc:mysql://localhost:3306/slave1?useUnicode=true&characterEncoding=utf8
      username: root
      password: root123!
      driver-class-name: com.mysql.cj.jdbc.Driver
    slave2:
      jdbc-url: jdbc:mysql://localhost:3306/slave2?useUnicode=true&characterEncoding=utf8
      username: root
      password: root123!
      driver-class-name: com.mysql.cj.jdbc.Driver

加载配置信息到IOC容器中

使用如下代码加载我们的配置信息:

@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
    return DataSourceBuilder.create().build();
}

@Bean(name = "slaveDataSource1")
@ConfigurationProperties(prefix = "spring.datasource.slave1")
public DataSource slaveDataSource1() {
    return DataSourceBuilder.create().build();
}

@Bean(name = "slaveDataSource2")
@ConfigurationProperties(prefix = "spring.datasource.slave2")
public DataSource slaveDataSource2() {
    return DataSourceBuilder.create().build();
}

配置动态数据源

在没有使用动态数据源的时候、我们只需要配置数据库相关配置即可正常使用。了解过SpringBoot装配原理的都知道、这是在应用启动过程中、自动装配了相关的bean

# AutoConfigureTestDatabase auto-configuration imports
org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase=\
org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

对应的Java代码如下:

@AutoConfiguration(before = SqlInitializationAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import(DataSourcePoolMetadataProvidersConfiguration.class)
public class DataSourceAutoConfiguration {

  @Configuration(proxyBeanMethods = false)
  @Conditional(EmbeddedDatabaseCondition.class)
  @ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
  @Import(EmbeddedDataSourceConfiguration.class)
  protected static class EmbeddedDatabaseConfiguration {

  }
  // 省略其他代码
}

因此在服务启动时、就需要排除掉相关配置@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})

当不使用默认配置就需要我们提供一个数据源、且是动态的。Spring 给我们提供了AbstractRoutingDataSource这个抽象类用于实现动态的数据源。

AbstractRoutingDataSource 实现了 DataSource 接口该接口提供了如下的信息:

Abstract DataSource implementation that routes getConnection() calls to one of various target DataSources based on a lookup key. The latter is usually (but not necessarily) determined through some thread-bound transaction context..

翻译过来就是

抽象的 DataSource 实现。基于不同的 look key 将getConnection调用到目标数据源上。通常(但不一定)是通过某些线程绑定的事务上下文来确定的

在这里我们梳理一下我们已经拥有的东西:

  • 我们定义了三个bean、这三个包含着具体去操作的数据库信息,

还有如下需要做的工作:

  • 实现一个动态的数据源类。那么代码如下
public class MyDynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return MyDynamicDataSourceHolder.getDataSourceType();
    }
}
public class MyDynamicDataSourceHolder {
    public static final Logger log = LoggerFactory.getLogger(MyDynamicDataSourceHolder.class);
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    public static void setDataSourceType(String dsType)
    {
        log.info("切换到 {} 数据源", dsType);
        CONTEXT_HOLDER.set(dsType);
    }
    public static String getDataSourceType()
    {
        return CONTEXT_HOLDER.get();
    }
    public static void clearDataSourceType()
    {
        CONTEXT_HOLDER.remove();
    }
}

为什么这里需要一个MyDynamicDataSourceHolder类呢?这是为了在切换数据源时调用,就是数据源的一种映射。只要能实现切换到功能、可以使用枚举、map等都可以

接着把我们实现的动态数据源加载到IOC容器中

/**
 * 创建时间: 2024年09月20日 13:05
 * 文件描述: 动态数据源配置
 */
@Configuration
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
public class DataSourceConfig {


    @Bean(name = "masterDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "slaveDataSource1")
    @ConfigurationProperties(prefix = "spring.datasource.slave1")
    public DataSource slaveDataSource1() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "slaveDataSource2")
    @ConfigurationProperties(prefix = "spring.datasource.slave2")
    public DataSource slaveDataSource2() {
        return DataSourceBuilder.create().build();
    }


    @Bean
    @Primary
    public DataSource dataSource() {
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("masterDataSource", masterDataSource());
        dataSourceMap.put("slaveDataSource1", slaveDataSource1());
        dataSourceMap.put("slaveDataSource2", slaveDataSource2());

        MyDynamicDataSource myDynamicDataSource = new MyDynamicDataSource();
        myDynamicDataSource.setDefaultTargetDataSource(masterDataSource());
        myDynamicDataSource.setTargetDataSources(dataSourceMap);

        return myDynamicDataSource;
    }
}

到这里已经提供了动态数据源切换的能力。可以在项目中使用了。

使用动态数据源

controller层定义如下三个接口

  @PostMapping("insert.do")
  public UserDO insert(@RequestBody UserDO userDO){

      log.info("user = {}",userDO);
      return userService.insertUser(userDO);
  }
  @PostMapping("insert_slave1.do")
  public UserDO insertSlave1(@RequestBody UserDO userDO){

      log.info("user = {}",userDO);
      return userService.insertUserBySlave1(userDO);
  }
  @PostMapping("insert_slave2.do")
  public UserDO insertSlave2(@RequestBody UserDO userDO){

      log.info("user = {}",userDO);
      return userService.insertUserBySlave2(userDO);
  }

代码很简单不做解释、service层代码如下:

@Override
  public UserDO insertUser(UserDO user) {
      log.info("before insert User = " + user);
      Integer insertRes = userMapper.insertUser(user);
      if (insertRes > 0) {
          log.info("after insert User = " + user);
      }

      return user;
  }

  @Override
  public UserDO insertUserBySlave1(UserDO user) {
      MyDynamicDataSourceHolder.setDataSourceType("slaveDataSource1");
      Integer insertRes = userMapper.insertUser(user);
      if (insertRes > 0) {
          log.info("after insert User = " + user);
      }
      MyDynamicDataSourceHolder.clearDataSourceType();
      return user;
  }

  @Override
  public UserDO insertUserBySlave2(UserDO user) {
      MyDynamicDataSourceHolder.setDataSourceType("slaveDataSource2");
      Integer insertRes = userMapper.insertUser(user);
      if (insertRes > 0) {
          log.info("after insert User = " + user);
      }
      MyDynamicDataSourceHolder.clearDataSourceType();
      return user;
  }

mapper.xml代码如下:

<insert id="insertUser" parameterType="com.mybatis.tutorial.domain.UserDO" useGeneratedKeys="true"
        keyProperty="userId">
    insert into user(user_name, password)
    values (#{userName}, #{password})
</insert>

运行项目、访问对应的接口可以看到符合预期效果。动态数据源起作用了。

动态数据源是如何切换的

为什么上面这样写就可以实现动态数据源的切换呢?

通过determineCurrentLookupKey()方法会返回一个String、这个String就是代表的数据源beanbeanName,那么是在哪里通过这个 beanName 加载到对应的数据源呢?

这就需要我们去看一抽象类AbstractRoutingDataSource的代码

// 决定使用哪一个dataSource  
protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    Object lookupKey = determineCurrentLookupKey();
    // 从resolvedDataSources获取数据源。
    DataSource dataSource = this.resolvedDataSources.get(lookupKey);
    if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
      dataSource = this.resolvedDefaultDataSource;
    }
    if (dataSource == null) {
      throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    }
    return dataSource;
  }
// 往resolvedDataSources填充数据
  @Override
  public void afterPropertiesSet() {
    if (this.targetDataSources == null) {
      throw new IllegalArgumentException("Property 'targetDataSources' is required");
    }
    this.resolvedDataSources = CollectionUtils.newHashMap(this.targetDataSources.size());
    this.targetDataSources.forEach((key, value) -> {
      Object lookupKey = resolveSpecifiedLookupKey(key);
      DataSource dataSource = resolveSpecifiedDataSource(value);
      this.resolvedDataSources.put(lookupKey, dataSource);
    });
    if (this.defaultTargetDataSource != null) {
      this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
    }
  }
// 获取数据库连接
  @Override
  public Connection getConnection(String username, String password) throws SQLException {
    return determineTargetDataSource().getConnection(username, password);
  }

可以看到是通过DataSource dataSource = this.resolvedDataSources.get(lookupKey);获取了数据源、而lookupKey正是MyDynamicDataSource.determineCurrentLookupKey()方法的返回值。也就是在这里实现了数据源的切换。

使用注解+Aop优化使用过程

在上面的代码里、涉及到数据源切换的都需要 MyDynamicDataSourceHolder.setDataSourceType("beanName");因此我们可以通过AOP来优化这一行代码。

如何切换到想要的数据源呢?则可以通过注解实现。注解代码如下:

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
public @interface DynamicDataSource {
    String value() default "masterDataSource";
}

环绕通知代码如下:

@Aspect
@Component
public class DataSourceAspect {
    @Pointcut("@annotation(com.mybatis.tutorial.config.datasource.DynamicDataSource)")
    private void dynamicDataSource(){
    }
    @Around("dynamicDataSource()")
    public Object around(ProceedingJoinPoint point) throws Throwable{
        MethodSignature signature = (MethodSignature) point.getSignature();
        DynamicDataSource dynamicDataSource = signature.getMethod().getAnnotation(DynamicDataSource.class);
        if(dynamicDataSource != null){
            MyDynamicDataSourceHolder.setDataSourceType(dynamicDataSource.value());
        }else {
            MyDynamicDataSourceHolder.setDataSourceType("masterDataSource");
        }
        try {
            return point.proceed();
        }finally {
            MyDynamicDataSourceHolder.clearDataSourceType();
        }
    }
}

测试代码、和预期一致。到此所有功能就已经完全实现了。

可以继续优化的地方:

  • 配置文件的读取可以写一个DataSourceProperties来接收配置值。
  • 可以考虑将这一个动态数据源封装为一个starter、这样在后续的项目过程中使用到了此功能就可以直接引入。
转载自:https://juejin.cn/post/7416292550754500658
评论
请登录