spring动态数据源和事务配合的调研

来源:互联网 发布:爱腾网络延长器接法图 编辑:程序博客网 时间:2024/05/20 16:45

Spring动态数据源配置

1. xml配置 [代码片段]

    <!--动态数据源-->    <bean id="dataSource" class="com.greenline.health.common.dbconfiguration.DynamicDataSource">        <property name="targetDataSources">            <map key-type="java.lang.String">                <entry key="ds001" value-ref="dataSource1"></entry>                <entry key="ds002" value-ref="dataSource2"></entry>            </map>        </property>        <property name="defaultTargetDataSource" ref="ds001"></property>    </bean>    <!-- DataSource定义。 -->    <bean name="dataSource1" class="org.apache.commons.dbcp.BasicDataSource"          destroy-method="close">        <property name="url" value="${jdbc.url}"/>        <property name="username" value="${jdbc.username}"/>        <property name="password" value="${jdbc.password}"/>    </bean>    <!-- DataSource定义。 --><bean name="dataSource2"  class="org.apache.commons.dbcp.BasicDataSource"          destroy-method="close">        <property name="url" value="${remote.jdbc.url}"/>        <property name="username" value="${remote.jdbc.username}"/>        <property name="password" value="${remote.jdbc.password}"/></bean><!--mybatis相关配置--><!-- 配置sqlSessionFactory -->    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">        <!-- 实例化sqlSessionFactory时需要使用上述配置好的数据源以及SQL映射文件 -->        <property name="dataSource" ref="dataSource"/>        <!-- 自动扫描/src/main/resources/sqlmap和sqlmap2/目录下的所有SQL映射的xml文件, 省掉Configuration.xml里的手工配置 value="classpath:me/gacl/mapping/*.xml"指的是classpath(类路径)下me.gacl.mapping包中的所有xml文件             UserMapper.xml位于me.gacl.mapping包下,这样UserMapper.xml就可以被自动扫描 -->        <property name="mapperLocations">            <array>                <value>classpath*:sqlmap/**/*.xml</value>                <value>classpath*:sqlmap2/**/*.xml</value>            </array>        </property>        <property name="plugins">            <list></list>        </property>    </bean>    <!-- 配置扫描器 -->    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">        <!-- 扫描me.gacl.dao这个包以及它的子包下的所有映射接口类 -->        <property name="basePackage" value="com.greenline.health.dal*"/>        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>    </bean><!--事务管理器--><!-- 配置Spring的事务管理器 --><bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">        <property name="dataSource" ref="dataSource"/></bean><!--事务拦截配置--> <!-- 拦截器方式配置事物 --><tx:advice id="transactionAdvice" transaction-manager="transactionManager">        <tx:attributes>            <tx:method name="add*" propagation="REQUIRED"/>            <tx:method name="append*" propagation="REQUIRED"/>            <tx:method name="insert*" propagation="REQUIRED"/>            <tx:method name="save*" propagation="REQUIRED"/>            <tx:method name="update*" propagation="REQUIRED"/>            <tx:method name="modify*" propagation="REQUIRED"/>            <tx:method name="edit*" propagation="REQUIRED"/>            <tx:method name="get*" propagation="SUPPORTS"/>            <tx:method name="find*" propagation="SUPPORTS"/>            <tx:method name="load*" propagation="SUPPORTS"/>            <tx:method name="search*" propagation="SUPPORTS"/>        </tx:attributes>    </tx:advice>    <aop:config>        <aop:pointcut id="transactionPointcut" expression="execution(* com.greenline.health.service..*Impl.*(..))"/>        <aop:advisor pointcut-ref="transactionPointcut" advice-ref="transactionAdvice"/> </aop:config>

2. 动态数据源用到的代码类

//========数据源获取==============public class DynamicDataSource extends AbstractRoutingDataSource {    @Override    protected Object determineCurrentLookupKey() {        return DataSourceContextHolder.getDataSourceType();    }}//数据源的拦截和切换ComponentAspectpublic class DBChangeInterceptor {    Logger logger = LoggerFactory.getLogger(DBChangeInterceptor.class);    @Pointcut(value = "execution(* com.greenline.health.dal..*.*(..))")    public void dbChangeBstKaPointCut() {    }    @Pointcut(value = "execution(* com.greenline.health.dal2..*.*(..))")    public void dbChangeStdLawPointCut() {    }    @Before(value = "dbChangeBstKaPointCut()")    public void changeBstKaDB(JoinPoint joinPoint) {        logger.debug("----------------调用ds001数据库连接----------------");        DataSourceContextHolder.setDataSourceType(DataSourceConstans.BTSKA);    }    @Before(value = "dbChangeStdLawPointCut()")    public void changeStdLawDB(JoinPoint joinPoint) {        logger.debug("----------------调用ds002数据库连接----------------");        DataSourceContextHolder.setDataSourceType(DataSourceConstans.STDLAW);    }//数据源切换工具public class DataSourceContextHolder {    private static final ThreadLocal contextHolder = new ThreadLocal(); // 线程本地环境    // 设置数据源类型    public static void setDataSourceType(String dataSourceType) {        contextHolder.set(dataSourceType);    }    // 获取数据源类型    public static String getDataSourceType() {        return (String) contextHolder.get();    }    // 清除数据源类型    public static void clearDataSourceType() {        contextHolder.remove();    }}

到此为止,配置已经完成,正常情况已经可以正确切换
为什么说正常情况?
很多时候,会遇到很多特别的情况,很简单的一个条件:加入事务

众所周知,一般事务配置在service层,但是动态数据源切换却是拦截在dao层
这时如果有事务可见问题就很难搞了.因为事务开启,调用进入service层首先去拿事务,
拿到事务之后,再去调用dao层对应的业务,这个时候才走到获取动态数据源. 获取动态数据源的时候,已经在事务中获取到了事务,就不会在重复获取连接了.此时,数据源切换失效.请看下面示例代码:

//service 层某一个方法 public Result<Boolean> updateCase() {        //add @1=ds001        //掉哟个数据源1        CaseBaseExample baseExample = new CaseBaseExample();        baseExample.createCriteria().andIdEqualTo(1);        caseBaseMapper.selectByExample(baseExample);        //add doc @2=ds002        //调用数据源2        DocNoticeExample noticeExample = new DocNoticeExample();        noticeExample.createCriteria().andNumNoticeIdEqualTo(2);        docNoticeMapper.selectByExample(noticeExample);        return Result.buildResult(ResponseCode.SUCCESS, "ok", "ok");    }
1 事务传播性=required 时 service获取事务初始化流程

根据上文配置,service层的update方法,事务传播性为 required
在进入这个方法之前,无论如何必须去拿一个事务,要么新建一个,要么用现有的.
请看如下调用逻辑:

每一个被事务拦截的方法,都要走这个流程,来获取事务. 关键点就在这里:
AbstractPlatformTransactionManager.getTransaction(TransactionDefinition definition)
该方法的核心代码如下:

// No existing transaction found -> check propagation behavior to find out how to proceed.        if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {            throw new IllegalTransactionStateException(                    "No existing transaction found for transaction marked with propagation 'mandatory'");        }        else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||                definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||            definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {            SuspendedResourcesHolder suspendedResources = suspend(null);            if (debugEnabled) {                logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition);            }            try {            //如果service 的事务传递性为required,则执行doBegin方法                boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);                DefaultTransactionStatus status = newTransactionStatus(                        definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);                doBegin(transaction, definition);                prepareSynchronization(status, definition);                return status;            }            catch (RuntimeException ex) {                resume(null, suspendedResources);                throw ex;            }            catch (Error err) {                resume(null, suspendedResources);                throw err;            }        }        else {            ///如果事务传递性是 suport 则执行这一个分支            // Create "empty" transaction: no actual transaction, but potentially synchronization.            boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);            return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);        }

required 类型的传播事务,代码流转如下

从上面的时序图能够发现,如果事务传播特性是required,则会执行doBegin去获取事务,也就是获取数据源

获取数据源的代码就在上图的最后一个环节,determineTargetDataSource中,详细代码如下

/**     * Retrieve the current target DataSource. Determines the     * {@link #determineCurrentLookupKey() current lookup key}, performs     * a lookup in the {@link #setTargetDataSources targetDataSources} map,     * falls back to the specified     * {@link #setDefaultTargetDataSource default target DataSource} if necessary.     * @see #determineCurrentLookupKey()     */    protected DataSource determineTargetDataSource() {        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");        //这个地方会调用自己实现类DynamicDataSource中的determineCurrentLookupKey方法,就是获取当前线程的数据源.即ds001或者ds002        Object lookupKey = determineCurrentLookupKey();        DataSource dataSource = this.resolvedDataSources.get(lookupKey);        //如果没有拿到当前的looupKey, 则根据以上xml配置里面的defaultTargetDataSource,获取默认连接,就是说,如果当前有设置的key就根据key获取,如果没有,就从动态数据源配置中获取默认的数据源连接. 本文中就是ds001        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;    }

总结:这种情况下,如果一个事务service中有多个数据源的dao操作,会导致找不到数据源.因为数据源是事务启动的时候已经找好了的.不会在执行dao层的时候,重新寻找了.这个时候就会出现类似Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'ds001.doc_notice' doesn't exist 的报错.

2 事务传播性=supported 时 service事务初始化流程

如果事务传播属性为supported,则进入service之前代码不会主动创建事务,但是会new一个事务上下文并且用来保存一些属性到当前的线程.时序图如下所示:

可以看到在这个流程中,创建的事务上下文并没有去执行 doBegin
来拿数据源链接,但是这种情况会不会表示 数据源的切换就没有问题了呢 .
不见得.

请看下文继续分析.

3 正式请求进入service之后的处理流程

上文分析了 在事务情况下,数据源和事务的关系 .下文我们看下,请求进入service之后,在执行dao层之前 的数据源切换流程.
在分析具体执行代码之前,请先看下面时序图:

A 拦截切换部分

这个过程比较简单, 一个普通的拦截器调用.会在获取数据库连接之前,调用线程中保存的数据源key的上下文.后面拿数据库连接,就根据 DataSourceContextHolder.setDataSourceType(DynamicDataSource.USER);这句代码对其的设定来获取的

B 获取数据源和执行DB操作部分[insert为例]

这个环节是执行数据源和查询的核心逻辑,通过层层的反射调用最终走到 SimpleExecutor.doUpdate 方法,在这里来获取所需要的一切

  @Override  public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {    Statement stmt = null;    try {      Configuration configuration = ms.getConfiguration();      StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);      //在这一句 要去获取connection了.....      stmt = prepareStatement(handler, ms.getStatementLog());      return handler.update(stmt);    } finally {      closeStatement(stmt);    }  }//正式获取 数据库连接 private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {    Statement stmt;    //这里回去调用获取连接方法    Connection connection = getConnection(statementLog);    stmt = handler.prepare(connection, transaction.getTimeout());    handler.parameterize(stmt);    return stmt;  }//BaseExecutor方法  protected Connection getConnection(Log statementLog) throws SQLException {  //不管走不走事务,都会从这里去获取数据源链接,此时会从这里请求SpringManagedTransaction的getConnection函数    Connection connection = transaction.getConnection();    if (statementLog.isDebugEnabled()) {      return ConnectionLogger.newInstance(connection, statementLog, queryStack);    } else {      return connection;    }  }//SpringManagedTransaction@(2_spring技术)Override  public Connection getConnection() throws SQLException {    if (this.connection == null) {      openConnection();    }    return this.connection;  }///SpringManagedTransactionprivate void openConnection() throws SQLException {//最终会从 DataSourceUtils那里拿到数据源连接    this.connection = DataSourceUtils.getConnection(this.dataSource);    this.autoCommit = this.connection.getAutoCommit();    this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);    if (LOGGER.isDebugEnabled()) {      LOGGER.debug(          "JDBC Connection ["              + this.connection              + "] will"              + (this.isConnectionTransactional ? " " : " not ")              + "be managed by Spring");    }  }

经过这么多道弯,最终去DataSourceUtils 那里获取数据库连接.在DataSourceUtils中进行了 事务和非事务的判断,并提供了不同情况下 ,怎么那数据库连接的方式.请看如下代码:

/**     * Actually obtain a JDBC Connection from the given DataSource.     * Same as {@link #getConnection}, but throwing the original SQLException.     * <p>Is aware of a corresponding Connection bound to the current thread, for example     * when using {@link DataSourceTransactionManager}. Will bind a Connection to the thread     * if transaction synchronization is active (e.g. if in a JTA transaction).     * <p>Directly accessed by {@link TransactionAwareDataSourceProxy}.     * @param dataSource the DataSource to obtain Connections from     * @return a JDBC Connection from the given DataSource     * @throws SQLException if thrown by JDBC methods     * @see #doReleaseConnection     */    public static Connection doGetConnection(DataSource dataSource) throws SQLException {        Assert.notNull(dataSource, "No DataSource specified");        //从事务上下文获取ConnectionHolder ,如果要执行的dao层方法没有事务,则这里获取的conHolder是空的        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);        if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {            conHolder.requested();            //如果service中第一次执行db操作此时还没有连接 ,此时hasConnection 是空的.会从datasource 中获取.            if (!conHolder.hasConnection()) {                logger.debug("Fetching resumed JDBC Connection from DataSource");                conHolder.setConnection(dataSource.getConnection());            }            //如果当前线程已经有了连接,直接返回. 这里就可以看出,如果存在事务,第二次拿取连接不是根据切换之后的key拿的,而是从事务上下文直接返回的.            return conHolder.getConnection();        }        // Else we either got no holder or an empty thread-bound holder here.        logger.debug("Fetching JDBC Connection from DataSource");        //如果没有事务 ,每次都是直接去根据当前线程上下文的key获取动态数据源的指定连接.        Connection con = dataSource.getConnection();        ///如果当前有事务上下文,则把之前拿到的连接绑定到当前的事务中去.        if (TransactionSynchronizationManager.isSynchronizationActive()) {            logger.debug("Registering transaction synchronization for JDBC Connection");            // Use same Connection for further JDBC actions within the transaction.            // Thread-bound object will get removed by synchronization at transaction completion.            ConnectionHolder holderToUse = conHolder;            if (holderToUse == null) {                holderToUse = new ConnectionHolder(con);            }            else {                holderToUse.setConnection(con);            }            holderToUse.requested();            TransactionSynchronizationManager.registerSynchronization(                    new ConnectionSynchronization(holderToUse, dataSource));            holderToUse.setSynchronizedWithTransaction(true);            if (holderToUse != conHolder) {                TransactionSynchronizationManager.bindResource(dataSource, holderToUse);            }        }        return con;    }

经过以上的分析,可以看出 ,动态数据源只有在没有事务的情况下 ,能正常切换. 不支持分布式事务.如果再一个有多个数据库操作的service中还加了事务,则很有可能会报错.

总结

使用动态数据源要注意什么情况才能安全的使用它呢?

1. 数据源用完之后要清理掉.所以对一个dao操作,要加两个拦截执行之前 设置数据源的key,执行sql之后,清空sql的key. 为什么这么做?
如果用了之后不清理,比如上次用过之后,没有置空数据源的key,然后执行到下一个service时候,当前的key是ds002, 如果要执行的这个service是包含事务的,并且是操作ds001数据库的,这个时候事务的数据源是ds002,将无法操作事务中的dao函数.
数据源拦截代码改造如下

//拦截以后设置数据源@Before(value = "dbChangeBstKaPointCut()")    public void changeBstKaDB(JoinPoint joinPoint) {        logger.debug("----------------调用bstka数据库连接----------------");        DataSourceContextHolder.setDataSourceType(DynamicDataSource.USER);    }        //用过之后清理    @AfterReturning(value = "dbChangeBstKaPointCut()")    public void resetKaHolder(JoinPoint joinPoint) {        logger.debug("----------------清除bstka数据库连接----------------");        DataSourceContextHolder.clearDataSourceType();    }

2. 有多数据源的service不要使用事务,即便是 supported类型的传播性也会导致出错. 推荐不要用 事务拦截的tx:advice节点里面 不要配置 <tx:method name="*" propagation="SUPPORTS"/> 这样的话 ,所有的方法都会创建事务上下文,会让我们配置的数据源动态切换失效.

原创粉丝点击