这样玩儿spring多数据源

 

分布式服务,经常是一个服务对应一个库,但也有的时候一个服务需要用到两个库,这个时候,一个服务就需要配置两个数据源,来支持业务的需要。


单数据源配置如下:

/**
 * Druid的配置文件
 * 用于监控数据库SQL
 */
@Configuration
@Slf4j
@RefreshScope
public class DruidConfiguration {

    @Value("${spring.datasource.url}")
    private String dbUrl;
    ………………………………

    @Value("${spring.datasource.username}")
    private String username;

   

    /**
     * 配置数据库连接池
     * @return
     */
//    @Bean     //声明其为Bean实例
//    @Primary  //在同样的DataSource中,首先使用被标注的DataSource
    public DataSource dataSource(){
        DruidDataSource datasource = new DruidDataSource();

        log.info(">>>>>>>>>>>>>开始配置druid连接池<<<<<<<<<<<<<<<<<<");

        log.info(">>>>>>>>>>>>>>>>>>>>数据库连接 : dbUrl<<<<<<<<<<<<<<<<<<<"+dbUrl);

        datasource.setUrl(this.dbUrl);
        datasource.setUsername(username);
        datasource.setPassword(password);
        datasource.setDriverClassName(driverClassName);

        //configuration
        datasource.setInitialSize(initialSize);
        datasource.setMinIdle(minIdle);
        datasource.setMaxActive(maxActive);
        datasource.setMaxWait(maxWait);
        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        datasource.setValidationQuery(validationQuery);
        datasource.setTestWhileIdle(testWhileIdle);
        datasource.setTestOnBorrow(testOnBorrow);
        datasource.setTestOnReturn(testOnReturn);
        datasource.setPoolPreparedStatements(poolPreparedStatements);
        datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
        try {
            datasource.setFilters(filters);
        } catch (SQLException e) {
            log.error("druid configuration initialization filter", e);
        }
        datasource.setConnectionProperties(connectionProperties);

        return datasource;
    }

}

单数据源时,在服务启动时,会加载数据源配置类,将config服务中的数据源配置信息加载出来。


Q:多数据源是不应该配置多个数据源类嘞?

A:当然是配置多个喽,多数据源类如下:

@Configuration
@Slf4j
@RefreshScope
public class SecondConfig {


    @Value("${spring.appprofiledatasource.url}")
    private String dbUrl;
    …………………………………………

    @Value("${spring.appprofiledatasource.username}")
    private String username;


    private DataSource ds = null;

    /**
     * 配置数据库连接池
     * @return
     */

    public DataSource dataSource(){
        if(ds != null) return ds;
        DruidDataSource datasource = new DruidDataSource();

        log.info(">>>>>>>>>>>>>开始配置AppProfileDataSource连接池<<<<<<<<<<<<<<<<<<");

        log.info(">>>>>>>>>>>>>>>>>>>>数据库连接 : dbUrl<<<<<<<<<<<<<<<<<<<"+dbUrl);

        datasource.setUrl(this.dbUrl);
        datasource.setUsername(username);
        datasource.setPassword(password);
        datasource.setDriverClassName(driverClassName);

        //configuration
        datasource.setInitialSize(initialSize);
        datasource.setMinIdle(minIdle);
        datasource.setMaxActive(maxActive);
        datasource.setMaxWait(maxWait);
        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        datasource.setValidationQuery(validationQuery);
        datasource.setTestWhileIdle(testWhileIdle);
        datasource.setTestOnBorrow(testOnBorrow);
        datasource.setTestOnReturn(testOnReturn);
        datasource.setPoolPreparedStatements(poolPreparedStatements);
        datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
        try {
            datasource.setFilters(filters);
        } catch (SQLException e) {
            log.error("druid configuration initialization filter", e);
        }
        datasource.setConnectionProperties(connectionProperties);

        ds=datasource;
        return ds;
    }

}

Q:此时有两数据源后,他俩谁首先加载呢?

A:由主数据源登场来指挥

@Configuration
@ConditionalOnClass({DruidConfiguration.class, AppProfileConfig.class})
public class PrimaryDataSource {

    @Autowired
    DruidConfiguration mysql;
    @Autowired
    SecondConfig odps;

    @Bean
    @Primary
    public DataSource dataSource() {
        MultipleDataSource dynamicDataSource = new MultipleDataSource();
        // 默认数据源
        DataSource o = odps.dataSource();
        DataSource m = mysql.dataSource();
        dynamicDataSource.setDefaultTargetDataSource(m);
        // 配置多数据源
        Map<Object, Object> datasources = new HashMap<>();
        //datasources.put("dataSource", mysql);
        datasources.put("secondDataSource", o);
        dynamicDataSource.setTargetDataSources(datasources);
        return dynamicDataSource;
    }

}

如何保证主数据源可以指挥DruidOnfiguration和SecondConfig呢?此时就靠@Primary了,单数据源时,原本也有@Primary注解的,但是配置了多数据源后,@Primary注解就改放到主数据源中了。


Q:接下来的问题就是在使用时如何切换数据源呢?

A:继承spring的AbstractRountingDataSource来定义自己的动态数据源,这样就可以根据需要动态切换不同数据库的数据源了。

public class MultipleDataSource extends AbstractRoutingDataSource {
    /**
	 * 实现AbstractRoutingDataSource的抽象方法,获取与数据源相关的key
	 * 跟进源码可以发现,此key与数据源绑定的key值,然后用于获取连接
	 */
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceHolder.getDataSourceType();
    }
}

与数据源相关的key,我们是在DataSourceHolder类中用ThreadLocal来保存的,

/**
 * 保存当前线程数据源的key
 */
public final class DataSourceHolder {

    private static final ThreadLocal<String> HOLDER = new InheritableThreadLocal<>();

    /**
     * 绑定当前线程数据源路由的key
     * 在使用完成之后,必须调用removeRouteKey()方法删除
     */
    public static void setDataSourceType(String dataSourceType) {
        HOLDER.set(dataSourceType);
    }

    /**
     * 获取当前线程的数据源路由的key
     * @return
     */
    public static String getDataSourceType() {
        return HOLDER.get();
    }

    public static void clearDataSourceType() {
        HOLDER.remove();
    }
}

Q:那么问题又来了,数据源key,啥时候传进来嘞?

A:此时我们就需要使用spring的AOP编程在业务逻辑方法运行前将当前方法使用数据源的key从业务逻辑方法上自定义注解中解析数据源key,并添加到DataSourceHolder中。

切面类如下:

@Aspect
@Order(1)
@Component
public class MyAspect {


    //指定要切的自定义注解类
    @Pointcut("@annotation(cn.aa.common.aspect.TargetDataSource)")
    public void pointcut() {
    }

//    @Before("execution(* cn.lefull.service..*(..))")
    @Before("@annotation(cn.lefull.common.aspect.TargetDataSource)")
    public void before(JoinPoint point) throws Exception {
        TargetDataSource targetDataSource = AnnotationUtils.findAnnotation(((MethodSignature) point.getSignature()).getMethod(), TargetDataSource.class);
        if (Objects.isNull(targetDataSource)) {
            targetDataSource = AnnotationUtils.findAnnotation(point.getClass(), TargetDataSource.class);
            if (Objects.isNull(targetDataSource)) {
                Object proxy = point.getThis();
                Field h = FieldUtils.getDeclaredField(proxy.getClass().getSuperclass(), "h", true);
                AopProxy aopProxy = (AopProxy) h.get(proxy);
                ProxyFactory advised = (ProxyFactory) FieldUtils.readDeclaredField(aopProxy, "advised", true);
                Class<?> targetClass = advised.getProxiedInterfaces()[0];
                targetDataSource = AnnotationUtils.findAnnotation(targetClass, TargetDataSource.class);
            }
        }
        DataSourceHolder.setDataSourceType(Objects.nonNull(targetDataSource) ? targetDataSource.value() : "dataSource");
    }

    @After("pointcut()")
    public void after(JoinPoint point) {
        DataSourceHolder.clearDataSourceType();
    }
}

自定义注解类:

@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {
    String value() default "dataSource";
}

应用

//在方法上加上自定义注解,值即为数据源key
@Override
@TargetDataSource("secondDataSource")
public int updateByPrimaryKey(IdfaInfo idfaInfo) {
     return idfaInfoRepository.updateByPrimaryKey(idfaInfo);
}

使用时,在方法上加上自定义注解,并将值赋为第二数据源,然后在方法执行时,便可以加载第二数据源。

以上的配置,切面注解只能加载方法上,加到类上无法获取数据源,想要加到类上,需要修改切面类。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木子松的猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值