AbstractRoutingDataSource实现动态数据源切换

一、AbstractRoutingDataSource介绍

Spring-jdbc 提供了AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。

AbstractRoutingDataSource的getConnection() 方法根据查找 lookup key 键对不同目标数据源的调用,通常是通过(但不一定)某些线程绑定的事物上下文来实现。

AbstractRoutingDataSource的多数据源动态切换的核心逻辑是:在程序运行时,把数据源数据源通过 AbstractRoutingDataSource 动态织入到程序中,灵活的进行数据源切换。
基于AbstractRoutingDataSource的多数据源动态切换,可以实现读写分离,这么做缺点也很明显,无法动态的增加数据源。

实现逻辑:

定义DynamicDataSource类继承抽象类AbstractRoutingDataSource,并实现了determineCurrentLookupKey()方法。
把配置的多个数据源会放在AbstractRoutingDataSource的 targetDataSources和defaultTargetDataSource中,然后通过afterPropertiesSet()方法将数据源分别进行复制到resolvedDataSources和resolvedDefaultDataSource中。
调用AbstractRoutingDataSource的getConnection()的方法的时候,先调用determineTargetDataSource()方法返回DataSource在进行getConnection()。

二、实现步骤

1. 多数据源配置

spring.datasource.master.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.master.hikari.minimum-idle=5
spring.datasource.master.hikari.maximum-pool-size=30
spring.datasource.master.hikari.auto-commit=true
spring.datasource.master.hikari.idle-timeout=30000
spring.datasource.master.hikari.pool-name=SCMMasterHikariCP
spring.datasource.master.hikari.max-lifetime=1800000
spring.datasource.master.hikari.connection-timeout=30000
spring.datasource.master.hikari.connection-test-query=SELECT 1

spring.datasource.slave.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.slave.hikari.minimum-idle=2
spring.datasource.slave.hikari.maximum-pool-size=10
spring.datasource.slave.hikari.auto-commit=true
spring.datasource.slave.hikari.idle-timeout=30000
spring.datasource.slave.hikari.pool-name=SCMSlaveHikariCP
spring.datasource.slave.hikari.max-lifetime=1800000
spring.datasource.slave.hikari.connection-timeout=30000
spring.datasource.slave.hikari.connection-test-query=SELECT 1

spring.datasource.master.jdbc-url=jdbc:mysql://localhost:3306/supply_center?useSSL=false&characterEncoding=UTF-8&autoReconnect=true
spring.datasource.master.username=root
spring.datasource.master.password=1234
spring.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver

spring.datasource.slave.jdbc-url=jdbc:mysql://localhost:3306/supply_center?useSSL=false&characterEncoding=UTF-8&autoReconnect=true
spring.datasource.slave.username=root
spring.datasource.slave.password=1234
spring.datasource.slave.driver-class-name=com.mysql.cj.jdbc.Driver
@Configuration
public class DataSourceConfig {

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

    @Bean(name = "slaveDataSource")
    @ConfigurationProperties("spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
        @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DBTypeEnum.MASTER, masterDataSource);
        targetDataSources.put(DBTypeEnum.SLAVE, slaveDataSource);
        MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();
        myRoutingDataSource.setDefaultTargetDataSource(masterDataSource);//设置默认数据源
        myRoutingDataSource.setTargetDataSources(targetDataSources);//设置数据源
        return myRoutingDataSource;
    }
}

2. Mybatis datasource配置

@EnableTransactionManagement
@Configuration
public class MyBatisConfig {
    @Resource(name = "myRoutingDataSource")
    private DataSource myRoutingDataSource;

    @Autowired
    private MybatisProperties mybatisProperties;

    @Bean
    public SqlSessionFactory sqlSessionFactory() throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
        sqlSessionFactoryBean.setMapperLocations(mybatisProperties.resolveMapperLocations());
        sqlSessionFactoryBean.setConfiguration(mybatisProperties.getConfiguration());
        return sqlSessionFactoryBean.getObject();
    }

    @Bean
    public PlatformTransactionManager platformTransactionManager() {
        return new DataSourceTransactionManager(myRoutingDataSource);
    }
}

3. 添加枚举类定义数据源类型

public enum DBTypeEnum {
    MASTER, SLAVE;
}

4. ThreadLocal实现当前线程设置数据源

@Slf4j
public class DBContextHolder {

    private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();

    public static void set(DBTypeEnum dbType) {
        contextHolder.set(dbType);
    }

    public static DBTypeEnum get() {
        return contextHolder.get();
    }

    public static void clear() {
        contextHolder.remove();
    }

    public static void master() {
        set(DBTypeEnum.MASTER);
        log.info("change Data Source to:{}", get());
    }

    public static void slave() {
        set(DBTypeEnum.SLAVE);
        log.info("change Data Source to:{}", get());
    }

}

5. 自定义datasource路由

public class MyRoutingDataSource extends AbstractRoutingDataSource {
    /**
     * Determine the current lookup key. This will typically be
     * implemented to check a thread-bound transaction context.
     * 

Allows for arbitrary keys. The returned key needs * to match the stored lookup key type, as resolved by the * {@link #resolveSpecifiedLookupKey} method. */ @Override protected Object determineCurrentLookupKey() { return DBContextHolder.get()==null?DBTypeEnum.MASTER:DBContextHolder.get(); } }

6. 定义注解和切面实现切换数据源

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {

    DBTypeEnum type() default DBTypeEnum.SLAVE;

}
@Aspect
@Component
@Order(0)
public class DataSourceAspect {

    @Pointcut("(@annotation(com.thermofisher.dsc.scm.datasource.DataSource))")
    public void pointcut() {
    }

    @Before("pointcut()")
    public void read(JoinPoint joinPoint) {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        DataSource source = method.getAnnotation(DataSource.class);//获取注解内容
        switch (source.type()) {
            case MASTER:
                DBContextHolder.master();
                break;
            case SLAVE:
                DBContextHolder.slave();
                break;
            default:
                DBContextHolder.master();
        }
        log.info("当前数据源:{}", DBContextHolder.get());
    }

    @After("pointcut()")
    public void after(JoinPoint point) {
        //清理掉当前设置的数据源,让默认的数据源不受影响
        log.info("清理数据源 当前源:{}", DBContextHolder.get());
        DBContextHolder.clear();
    }

}

7. 使用

对应方法添加数据源注解,便可实现数据源灵活切换

 @Override
 @DataSource(type = DBTypeEnum.SLAVE)
 public List<CityModel> selectAllCityInfo() {
     return cityMapper.selectCityInfo(null);
 }

你可能感兴趣的:(数据库中间件)