Spring 动态数据源-主从数据库

Spring内置了一个AbstractRoutingDataSource,它可以把多个数据源配置成一个Map,根据不同的key返回不同的数据源。因为AbstractRoutingDataSource也是一个DataSource接口,因此,应用程序可以先设置好key, 访问数据库的代码就可以从AbstractRoutingDataSource拿到对应的一个真实的数据源,从而访问指定的数据库。

配置数据源

spring:
  master-datasource:
    username: root
    password: 123456;a
    driver-class-name: com.mysql.cj.jdbc.Driver
    jdbc-url: jdbc:mysql://localhost/test_master
    hikari:
      pool-name: HikariCP
      auto-commit: false
  slave-datasource:
    username: root
    password: 123456;a
    driver-class-name: com.mysql.cj.jdbc.Driver
    jdbc-url: jdbc:mysql://localhost/test_slave
    hikari:
      pool-name: HikariCP
      auto-commit: false

mybatis:
  mapper-locations: classpath:mapper/*.xml

数据源Config

@Slf4j
@Configuration
public class DataSourceConfig {
    /**
     * Master data source.
     */
    @Bean("masterDataSource")
    @ConfigurationProperties(prefix = "spring.master-datasource")
    public DataSource masterDataSource() {
        log.info("create master datasource...");
        return DataSourceBuilder.create().build();
    }

    /**
     * Slave (read only) data source.
     */
    @Bean("slaveDataSource")
    @ConfigurationProperties(prefix = "spring.slave-datasource")
    public DataSource slaveDataSource() {
        log.info("create slave datasource...");
        return DataSourceBuilder.create().build();
    }
}

继承AbstractRoutingDataSource 重写determineCurrentLookupKey

把两个真实的数据源代理为一个动态数据源:

public class RoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return "masterDataSource";
    }
}

配置主数据源

@Bean
    @Primary
    DataSource primaryDataSource(
            @Autowired @Qualifier("masterDataSource") DataSource masterDataSource,
            @Autowired @Qualifier("slaveDataSource") DataSource slaveDataSource
    ) {
        log.info("create routing datasource...");
        Map<Object, Object> map = new HashMap<>();
        map.put("masterDataSource", masterDataSource);
        map.put("slaveDataSource", slaveDataSource);
        RoutingDataSource routing = new RoutingDataSource();
        routing.setTargetDataSources(map);
        routing.setDefaultTargetDataSource(masterDataSource);
        return routing;
    }

RoutingDataSource配置好了,但是,路由的选择是写死的,即永远返回"masterDataSource"

存储动态选择的key

编写一个RoutingDataSourceContext,来设置并动态存储key

public class RoutingDataSourceContext implements AutoCloseable {

    // holds data source key in thread local:
    static final ThreadLocal<String> threadLocalDataSourceKey = new ThreadLocal<>();

    public static String getDataSourceRoutingKey() {
        String key = threadLocalDataSourceKey.get();
        return !StringUtils.hasText(key) ? "masterDataSource" : key;
    }

    public RoutingDataSourceContext(String key) {
        threadLocalDataSourceKey.set(key);
    }

    public void close() {
        threadLocalDataSourceKey.remove();
    }
}

修改RoutingDataSource,获取key的代码如下

public class RoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return RoutingDataSourceContext.getDataSourceRoutingKey();
    }
}

在某个地方,例如一个Controller的方法内部,就可以动态设置DataSource的Key

@Controller
public class MyController {
    @Get("/")
    public String index() {
        String key = "slaveDataSource";
        try (RoutingDataSourceContext ctx = new RoutingDataSourceContext(key)) {
            // TODO:
        }
    }
}

注解aop动态选择数据源

添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

切面代码

@Aspect
@Component
public class DataSourceAop {
    @Around("@annotation(sourceWith)")
    public Object routingWithDataSource(ProceedingJoinPoint joinPoint, sourceWith sourceWith) throws Throwable {
        String key = sourceWith.value();
        if (StringUtils.hasText(key)) {
            key += "DataSource";
        }
        try (RoutingDataSourceContext ctx = new RoutingDataSourceContext(key)) {
            return joinPoint.proceed();
        }
    }
}

对应的注解代码

@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface sourceWith {
    String value() default "master";
}

使用

在对应的controller或者dao,service上添加注解
示例:

@Mapper
public interface UserDao {
    @sourceWith(value = "slave")
    User getById(Integer id);

    @sourceWith(value = "master")
    Integer insert(User user);
}

注意:受Servlet线程模型的局限,动态数据源不能在一个请求内设定后再修改,也就是@sourceWith 不能嵌套。此外,@sourceWith@Transactional混用时,要设定AOP的优先级

posted @ 2022-10-13 16:33  小白不爱  阅读(219)  评论(0编辑  收藏  举报