DynamicDataSource多数据源+事务,数据源选择不生效的排查过程- 踩坑总结集锦 19(一周一更)

851 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第13天,点击查看活动详情

问题背景

我们有一个系统项目是采用多数据源的架构设计模式,一共有三个生产库,所以我们在底层dbcp链接数据库的时候选择了Dynamic来作为控制器,但是在一次需求开发过程中,却发现原本生效的控制器“失灵”了,导致了链接的生产库错误,从而找不到相应的表报错。那么问题出现在哪里呢?我们通过分析一步步解开真相。

问题复现

先来看下我们的配置文件

#jdbc1

mvn.pms.jdbc.driver.sale=com.mysql.jdbc.Driver

mvn.pms.jdbc.url.sale=jdbc:mysql://xxxxxx/test?

useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&zero

DateTimeBehavior=convertToNull

mvn.pms.jdbc.username.sale=root

mvn.pms.jdbc.password.sale=root

#jdbc2

mvn.pms.jdbc.driver.sku=com.mysql.jdbc.Driver

mvn.pms.jdbc.url.sku=jdbc:mysql://xxxxxx/test?

useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&zero

DateTimeBehavior=convertToNull

mvn.pms.jdbc.username.sku=root

mvn.pms.jdbc.password.sku=root

DynamicDataSource动态数据源配置

<bean id="saleDataSource" class="org.apache.commons.dbcp.BasicDataSource"> <property name="driverClassName" value="${pms.jdbc.driver.sale}"/>
<property name="url" value="${pms.jdbc.url.sale}"/>
<property name="username" value="${pms.jdbc.username.sale}"/>
<property name="password" value="${pms.jdbc.password.sale}"/>
</bean> <bean id="skuDataSource" class="org.apache.commons.dbcp.BasicDataSource"> <property name="driverClassName" value="${pms.jdbc.driver.sku}"/>
<property name="url" value="${pms.jdbc.url.sku}"/>
<property name="username" value="${pms.jdbc.username.sku}"/>
<property name="password" value="${pms.jdbc.password.sku}"/>
</bean> <bean id="dynamicDataSource"
    class="com.jd.o2o.pms.dao.mysql.dataSource.DynamicDataSource"> <property name="targetDataSources"> <map key-type="java.lang.String"> <entry key="1" value-ref="saleDataSource"/>
<entry key="2" value-ref="skuDataSource"/>
</map>
</property> <property name="defaultTargetDataSource" ref="saleDataSource"/>
</bean>

DynamicDataSource获取数据源路由:

public class DynamicDataSource extends AbstractRoutingDataSource {

    private static final ThreadLocal<String> dataSourceKey = new

            InheritableThreadLocal<String>();

    public static void setDataSourceKey(String dataSource) {

        dataSourceKey.set(dataSource);

    }

    public static String getDatasourcekey() {

        return dataSourceKey.get();

    }

    @Override

    protected Object determineCurrentLookupKey() {

        return dataSourceKey.get();

    }

}

上面以两个数据源为例子,当我们没有为DynamicDataSource指定key的时候,会去选择defaultTargetDataSource对应的数据源,即saleDataSource。当指定key的时候,则会去targetDataSources所对应的map中根据key去寻找对应的数据源。

我们当时在选择数据源的方法demo如下:

@Transactional

public void handle(List<A> elementArray) {

// 设置数据库为SKU库,对应的key为2

DynamicDataSource.setDataSourceKey(DataSourceEnum.SKU.getCode());

List<Long> keys =

elementArray.stream().map(A::getT1).collect(Collectors.toList());

businessMapper.deleteAll(keys);

businessMapper.insertBatch(elementArray);

}

是的,有个@Transactional的事务注解。结果在使⽤的过程中,发现提示表不存在,提示的是数据源sale中不存在a表。推测spring在选择数据源的时候,没有选择我们设置的sku数据源,还是采⽤了默认的sale数据源,那么问题出现在哪里呢?

问题原因

我们从从Spring开启事务的源码中,可以得到问题的原因:


protected void doBegin(Object transaction, TransactionDefinition definition) {

DataSourceTransactionManager.DataSourceTransactionObject txObject =

(DataSourceTransactionManager.DataSourceTransactionObject)transaction;

Connection con = null;

try {

// 开启时,如果没有connection,则获取当前的数据源,然后getConnection获取。

随后在事务中都会使⽤这个connectionif (txObject.getConnectionHolder() == null ||

txObject.getConnectionHolder().isSynchronizedWithTransaction()) {

Connection newCon = this.dataSource.getConnection();

if (this.logger.isDebugEnabled()) {

this.logger.debug("Acquired Connection [" + newCon + "]

for JDBC transaction");

}

txObject.setConnectionHolder(new ConnectionHolder(newCon),

true);

}

txObject.getConnectionHolder().setSynchronizedWithTransaction(true);

con = txObject.getConnectionHolder().getConnection();

// 其余操作

} catch (Throwable var7) {

if (txObject.isNewConnectionHolder()) {

DataSourceUtils.releaseConnection(con, this.dataSource);

txObject.setConnectionHolder((ConnectionHolder)null, false);

}

throw new CannotCreateTransactionException("Could not open JDBC

Connection for transaction", var7);

}

}

通过源码分析,我们可以得出⼀个结论:

在开启事务的时候,spring会去先获取当前数据源的connection,随后再整个事务⾥⾯复⽤这个connection,⽽由于我们是在事务内部设置DynamicDataSource的key

因此,在开启事务的时候,spring获取的是默认的数据源的connection,在这⾥实际上就是sale数据源所对应的connection,随后进⾏复⽤,因此我们操作sku数据源对应的表的时候就会发⽣报错。

解决方法:

在事务外去设置数据源

由于在开启事务的时候,会去选择数据源,并进⾏复⽤,因此直接在开启事务前,设置数据源即可。

public void business() {

// 设置数据源

DynamicDataSource.setDataSourceKey(DataSourceEnum.SKU.getCode());

// 开启事务

handle(elementArray);

}

采⽤注解+AOP的⽅式去设置数据源

如果选择在事务外设置数据源,感觉之后代码⾥⾯会到处充斥着设置数据源的代码。这个时候可以使⽤ 注解+AOP来进⾏管理。

参考blog.csdn.net/qq_31142553… 思路 是对于要设置数据源的⽅法加上注解,采⽤AOP扫描,如果发现了该注解,获取这个注解所对应的 key,然后设置数据源,随后执⾏⽅法。在该⽅法结束的时候,将数据源还原。

思考总结

事务的注解使用还是存在很多双刃剑的,所以在一些使用事务的地方一定要关注下相对应源码,多测试,多了解,才会避免踩坑。