天天看點

Springboot基于AbstractRoutingDataSource實作多資料源的動态切換

為了提高資料庫的查詢效率,利用資料庫主從機制,寫走主庫,查詢走從庫。如果隻是實作一主一從類似簡單的主從模式,可以繼承AbstractRoutingDataSource實作讀寫分離。而不需使用mycat,sharedingJDBC等資料庫插件。

分析AbstractRoutingDataSource可知,defaultTargetDataSource,表示預設的資料源;targetDataSources表示配置的所有資料源集合;afterPropertiesSet方法spring bean對象初始化方法,會把targetDataSources和defaultTargetDataSource,設定為resolvedDataSources和resolvedDefaultDataSource。getConnection()擷取jdbc的連接配接,并通過determineTargetDataSource()擷取指定的資料源,AbstractRoutingDataSource使用模闆類的模式,在父類定義了determineCurrentLookupKey()虛拟方法,擷取lookupkey對象;其子類必須實作該方法。源碼如下:

public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {

    /**
     *配置的資料源
     */
	@Nullable
	private Map<Object, Object> targetDataSources;

    /**
     *預設資料源
     */
	@Nullable
	private Object defaultTargetDataSource;

    ......

     /**
     *spring InitializingBean 實作方法,bean初始化時調用
     */
    @Override
	public void afterPropertiesSet() {
		if (this.targetDataSources == null) {
			throw new IllegalArgumentException("Property 'targetDataSources' is required");
		}
		this.resolvedDataSources = new HashMap<>(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);
		}
	}

    /**
     *擷取jdbc連結時,調用determineTargetDataSource,擷取指定的資料
     */
    @Override
	public Connection getConnection() throws SQLException {
		return determineTargetDataSource().getConnection();
	}


    /**
     *determineCurrentLookupKey方法通過子類自定義實作,擷取lookupKey,然後從resolvedDefaultDataSource map對象中擷取資料源
     */
    protected DataSource determineTargetDataSource() {
		Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
		Object lookupKey = determineCurrentLookupKey();
		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;
	}

    /**
     *子類必須實作的擷取lookupKey的方法
     */
    protected abstract Object determineCurrentLookupKey();
}
           

建立DataSourceAddressEnum枚舉類,定義MASTER與SLAVE,路由名稱。代碼如下:

public enum DataSourceAddressEnum {

    /**
     * 主資料庫
     */
    MASTER,

    /**
     * 從資料庫
     */
    SLAVE;
}
           

建立DataSourceContextHolder,使用ThreadLocal,定義每次操作的類型枚舉,代碼如下:

public class DataSourceContextHolder {

    private static final ThreadLocal<DataSourceAddressEnum> CONTEXT_HOLDER = ThreadLocal.withInitial(() -> DataSourceAddressEnum.MASTER);

    public static void setCurrentDataSource(DataSourceAddressEnum dataSourceAddressEnum) {
        CONTEXT_HOLDER.set(dataSourceAddressEnum);
    }

    public static DataSourceAddressEnum getCurrentDataSource() {
        return CONTEXT_HOLDER.get();
    }

    public static void removeDataSource() {
        CONTEXT_HOLDER.remove();
    }
}
           

建立RoutingDataSourceWithAddress,繼承AbstractRoutingDataSource,實作determineCurrentLookupKey,即實作了可以根據DataSourceAddressEnum枚舉類實作資料源的動态路由,代碼如下:

public class RoutingDataSourceWithAddress extends AbstractRoutingDataSource {

    /**
     * @param defaultTargetDataSource 預設的 DataSource
     * @param targetDataSources       配置的所有 DataSource
     */
    public RoutingDataSourceWithAddress(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
    }

    /**
     *配置的資料源
     */
    @Override
    protected Object determineCurrentLookupKey() {

        DataSourceAddressEnum routingDataSourceAddressEnum = DataSourceContextHolder.getCurrentDataSource();
        if (log.isDebugEnabled()) {
            log.debug("routing data source address is {}", routingDataSourceAddressEnum.name());
        }
        return routingDataSourceAddressEnum;
    }
}
           

使用AOP+注解的方式,對指定的方法進行資料源動态切換的控制。建立RoutingDataSource注解,定義需要路由的資料源,建立RoutingDataSourceAOP定義資料源路由的切面操作,代碼如下:

/**
 * DataSource路由注解
 **/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RoutingDataSource {

    /**
     * 路由的DataSource位址,預設為MASTER
     */
    DataSourceAddressEnum value() default DataSourceAddressEnum.MASTER;
}

/**
 * RoutingDataSource 的aop攔截
 **/
@Aspect
@Component
@Order(10000)
@Slf4j
public class RoutingDataSourceAOP {

    @Pointcut("@annotation(com.kuqi.mall.demo.conmon.datasource.RoutingDataSource)|| @within(com.kuqi.mall.demo.conmon.datasource.RoutingDataSource)")
    public void routingDataSourcePointcut() {
    }

    @Around("routingDataSourcePointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        RoutingDataSource routerDataSource = method.getAnnotation(RoutingDataSource.class);
        // 如果沒有設定則預設為 MASTER
        DataSourceAddressEnum dataSourceAddressEnum = Objects.isNull(routerDataSource) ? DataSourceAddressEnum.MASTER : routerDataSource.value();
        DataSourceContextHolder.setCurrentDataSource(dataSourceAddressEnum);
        try {
            return joinPoint.proceed();
        } finally {
            DataSourceContextHolder.removeDataSource();
        }
    }
}
           

建立基于Springboot的自動配置類RoutingDataSourceAutoConfiguration,隻要配置了master和slave的屬性檔案,和mybatis屬性檔案,就可以自動啟動配置。RoutingDataSourceAutoConfiguration源碼如下:

@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class, DruidDataSource.class})
@EnableConfigurationProperties(MybatisProperties.class)
public class RoutingDataSourceAutoConfiguration {

    /**
     * 配置master資料源
     */
    @Bean(name = "masterDataSource", initMethod = "init", destroyMethod = "close")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.druid.master")
    public DataSource masterDataSource() {
        DataSource dataSource = DataSourceBuilder.create(this.getClass().getClassLoader())
                .type(com.alibaba.druid.pool.DruidDataSource.class).build();
        return dataSource;
    }

    /**
     * 配置slave資料源
     */
    @Bean(name = "slaveDataSource", initMethod = "init", destroyMethod = "close")
    @ConfigurationProperties(prefix = "spring.datasource.druid.slave")
    public DataSource slaveDataSource() {
        DataSource dataSource = DataSourceBuilder.create(this.getClass().getClassLoader())
                .type(com.alibaba.druid.pool.DruidDataSource.class).build();
        return dataSource;
    }

    /**
     * 初始化路由DataSource
     */
    @Bean(name = "routingDataSourceWithAddress")
    public DataSource dataSource(
            @Autowired @Qualifier("masterDataSource") DataSource masterDataSource,
            @Autowired @Qualifier("slaveDataSource") DataSource slaveDataSource) {

        DataSource defaultTargetDataSource;
        Map<Object, Object> targetDataSources = ImmutableMap.of(
                DataSourceAddressEnum.MASTER, defaultTargetDataSource = masterDataSource,
                DataSourceAddressEnum.SLAVE, slaveDataSource);
        return new RoutingDataSourceWithAddress(defaultTargetDataSource, targetDataSources);
    }

    /**
     * 使用SqlSessionFactoryBean配置MyBatis的SqlSessionFactory
     **/
    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(
            @Autowired @Qualifier("routingDataSourceWithAddress") DataSource routingDataSourceWithAddress,
            @Autowired MybatisProperties mybatisProperties,
            @Autowired ResourceLoader resourceLoader) throws Exception {

        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(routingDataSourceWithAddress);
        // 設定configuration
        org.apache.ibatis.session.Configuration configuration = mybatisProperties.getConfiguration();
        factory.setConfiguration(configuration);
        // 設定SqlSessionFactory屬性
        String configLocation;
        if (StringUtils.isNotBlank(configLocation = mybatisProperties.getConfigLocation())) {
            factory.setConfigLocation(resourceLoader.getResource(configLocation));
        }
        Resource[] resolveMapperLocations;
        if (ArrayUtils.isNotEmpty(resolveMapperLocations = mybatisProperties.resolveMapperLocations())) {
            factory.setMapperLocations(resolveMapperLocations);
        }
        String typeHandlersPackage;
        if (StringUtils.isNotBlank(typeHandlersPackage = mybatisProperties.getTypeHandlersPackage())) {
            factory.setTypeHandlersPackage(typeHandlersPackage);
        }
        String typeAliasesPackage;
        if (StringUtils.isNotBlank(typeAliasesPackage = mybatisProperties.getTypeAliasesPackage())) {
            factory.setTypeAliasesPackage(typeAliasesPackage);
        }
        return factory.getObject();
    }

    /**
     * 使用routingDataSourceWithAddress配置資料庫事務
     */
    @Bean
    @ConditionalOnMissingBean
    public DataSourceTransactionManager dataSourceTransactionManager(
            @Autowired @Qualifier("routingDataSourceWithAddress") DataSource routingDataSourceWithAddress) {
        return new DataSourceTransactionManager(routingDataSourceWithAddress);
    }

    /**
     * 程式設計式事務
     */
    @Bean
    public TransactionTemplate transactionTemplate(
            @Autowired @Qualifier("dataSourceTransactionManager") PlatformTransactionManager platformTransactionManager) {
        return new TransactionTemplate(platformTransactionManager);
    }

    @Bean
    @ConditionalOnMissingBean(RoutingDataSourceAOP.class)
    public RoutingDataSourceAOP rRoutingDataSourceAOP() {
        return new RoutingDataSourceAOP();
    }
}
           

資料源使用DruidDataSource,詳細的配置檔案,master和slave資料源,可以配置不同的資料庫用來測試,實際開發中,配置為滿足資料庫主從複制的配置,配置代碼如下:

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      master:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/kuqi_mall?autoReconnectForPools=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: root
      slave:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/kuqi_mall?autoReconnectForPools=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
        username: root
        password: root
           

在springboot的啟動類中,屏蔽DruidDataSourceAutoConfigure自動配置類,就能啟動。啟動類代碼,以及操作示例代碼如下:

/**
 * springboot啟動類exclude DruidDataSourceAutoConfigure
 */
@SpringBootApplication(exclude = DruidDataSourceAutoConfigure.class)
@MapperScan(basePackages = {"com.kuqi.mall.demo.dao"})
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

    
     /**
     * 注解定義動态資料源操作
     */
    @Override
    @RoutingDataSource(DataSourceAddressEnum.SLAVE)
    public CouponBo getFromSlave(Long id) {
        return get(id);
    }
           

完整示例代碼,可以參考連結spring-boot-mall項目https://github.com/alldays/spring-boot-mall