Spring嵌套事物下多数据源造成的事物不起作用

        前些天发现线上友商企业账户余额不足,只剩1.5元,但是还出现了向外派发优惠券的情况,通过大日志平台查询之后发现,企业锁定金额操作的时候判断的时候虽然判断出了余额不足的状况,并且抛出了自定义异常,但是之前派发优惠券的操作没有回滚,派发优惠券是另外一个方法,有事物注解。当时就很纳闷了,一番情景还原之后,发现了问题。

先说一下项目环境:

采用spring管理事物,ibatis,双数据源,dubbo引用其他服务,数据库为oracle,支持事物。

事故现场:

public void yhqShopExchange(YhqExchangeMqVo yhqExchangeMqVo) {

     ...

     try {

         ...

         //这个是事物开始的地方

         yhqShopExchangeRecordService.exchangeYhq(record, details);

     } catch (BusinessException e) {

         log.error(e.getMessage());

     } catch (Exception e) {

         log.error( e.getMessage());

     }

}

方法 yhqShopExchange()是入口,里面是一些逻辑处理,没有加上事物注解@Transactional,事物开始是在yhqShopExchangeRecordService.exchangeYhq(record, details)里,这个方法如下:

@Transactional
public void exchangeYhq(YhqExchangeRecord record, List<YhqExchangeDetail> details) {
	...
	try {
        ...
		// 生成优惠券信息
		yhqTicketInfoService.productYhqTicketInfo(x, x, x);	
		...		
		// 扣减企业账户锁定金额,这里是出错的地方,是本service的另外一个方法
        updateAccountInfo(x, x,x);
	} catch(BusinessException e) {
		log.error(...);
		throw e;
	} catch(Exception e){
		log.error(...);
		throw new BusinessException(SysErrorConfig.SYS_0000000001);
	}
}

这个方法在执行兄弟方法updateAccountInfo()的时候抛了个异常,兄弟方法也带有事物,如下:

@Transactional
public void updateAccountInfo(String custmoterCode, long amount, String recordId) {
	...
	if (result < 0) {
        log.error(...);
        //程序走到这里,抛出异常
		throw new BusinessException(...);
	} else {
        ...
	}
    ...
}

 

兄弟方法抛出异常之后,被外层方法catch到,记录异常并继续抛出异常,这时候外层事物是应该工作的时候了。

1.从事物传递性角度来分析

来看一下这两层事物,外层事物和嵌套事物都是默认的传递规则 PROPAGATION_REQUIRED ,即:如果方法运行时,已经处在一个事务中,那么加入到这个事务,否则自己新建一个新的事务

所以网上有些说调用兄弟方法事物不起作用并不适合这里,1,兄弟方法也带有事物,不符。2,这里是后调用兄弟方法,不符。所以初步判断兄弟方法不是事物没回滚的症结所在。

关于兄弟方法带事物嵌套的相关文章,这里如果是同一种事物传递规则则不用使用代理明确调用

2.从异常捕获角度来分析

再来看一下兄弟方法抛出的异常,网上也有说异常如果被处理则事物不生效,很多人会有疑问,处理是怎么被处理并没有明确,catch到算不算处理?其实不是的,这里证实一下:

a .  在外层方法exchangeYhq()里,去掉try catch之后,数据没有回滚。

@Transactional
public void exchangeYhq(YhqExchangeRecord record, List<YhqExchangeDetail> details) {
	...
	//try {
        ...
		// 生成优惠券信息
		yhqTicketInfoService.productYhqTicketInfo(x, x, x);	
		...		
		// 扣减企业账户锁定金额,这里是出错的地方,是本service的另外一个方法
        updateAccountInfo(x, x,x);
	//} catch(BusinessException e) {
		//log.error(...);
		//throw e;
	//} catch(Exception e){
		//log.error(...);
		//throw new BusinessException(SysErrorConfig.SYS_0000000001);
	//}
}

b .  在外层方法exchangeYhq()里,catch到异常并打印之后,抛出一个new的异常,数据还是没有回滚。

@Transactional
public void exchangeYhq(YhqExchangeRecord record, List<YhqExchangeDetail> details) {
	...
	try {
        ...
		// 生成优惠券信息
		yhqTicketInfoService.productYhqTicketInfo(x, x, x);	
		...		
		// 扣减企业账户锁定金额,这里是出错的地方,是本service的另外一个方法
        updateAccountInfo(x, x,x);
	} catch(BusinessException e) {
		log.error(...);
		throw new BusinessException("这是个异常");
	} catch(Exception e){
		log.error(...);
		throw new BusinessException(SysErrorConfig.SYS_0000000001);
	}
}

所以问题也不在于try与不try,不在于异常处理没被处理,不在于是不是new的,只要最终把异常抛出,就OK。

3.从异常类型来分析

从异常类型来分析,@Transactional注解并没有绑定回滚异常,所以默认在碰到RuntimeException及其子类后就会回滚,这里抛出的异常继承自RuntimeExceotion,定义如下:

public class BusinessException extends RuntimeException {
	private static final long serialVersionUID =  ... ;
	...
}

所以也不是异常类型的问题

那么从代码角度来看,还有什么问题?

没有头绪的时候,就换换角度,这是一个好的习惯。

4.从数据源来分析

来看一下关键配置,是不是配置出错了造成事物不起作用?

Spring xml配置已经越来越简单

<bean id="dataSource"  class="org.springframework.jndi.JndiObjectFactoryBean"> 
    <property name="jndiName">  
           <value>${dataSource.dataSource}</value>  
    </property>
</bean>

<bean id="dataSourceHx"  class="org.springframework.jndi.JndiObjectFactoryBean"> 
    <property name="jndiName">  
           <value>${dataSource.dataSourceHx}</value>  
    </property>
</bean>

<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<property name="dataSource" ref="dataSource" />
</bean>

<bean id="transactionManagerHx" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<property name="dataSource" ref="dataSourceHx" />
</bean>

<aop:aspectj-autoproxy proxy-target-class="true"/>
<tx:annotation-driven transaction-manager="transactionManager"/>
<tx:annotation-driven transaction-manager="transactionManagerHx"/>

<bean id="sqlMap"
	class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
	<property name="configLocation">
		<value>classpath:configs/ibatis/SqlMapConfig.xml</value>
	</property>
	<property name="dataSource">
		<ref bean="dataSource" />
	</property>
</bean>

<bean id="sqlMapB"
	class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
	<property name="configLocation">
		<value>classpath:configs/ibatis/SqlMapConfigB.xml</value>
	</property>
	<property name="dataSource">
		<ref bean="dataSourceHx" />
	</property>
</bean>

 

需要注意:

这里配置了多个数据源,有两个事物管理器。

在Spring里,如果注解@Transactional没有指定事物管理器,那么会使用默认事物管理器,默认事物管理器在配置里,读取到的第一个事物管理器即为默认事物管理器,这里为transactionManager。

 

数据没有回滚,会不会是受数据源影响?

往里翻yhqTicketInfoService.productYhqTicketInfo(x, x, x)方法

@Transactional
public void productYhqTicketInfo(x,x, x) {
	// 派发优惠券
    //这里用sqlMapB去操作优惠券
}

 

发现派发优惠券的时候,居然用的是另外一套sqlMapB去读写数据库,而sqlMapB对应的是dataSourceHx数据源。

而事物管理器transactionManager对应的数据源是dataSource!如果是用dataSourceHx来修改数据,自然无法回滚数据。到这里,真相大白。

那么,如何解决这个问题呢,注解@Transactional早就想到这点了,在注解@Transactional里用value就可以指定数据源,如:@Transactional(value="transactionManagerHx")

这样,事物就可以起作用了,要确保同一事物环境使用的数据源相同。

下面是修正后的代码

事物开始的地方:

@Transactional(value="transactionManagerHx",rollbackFor=Exception.class)
public void exchangeYhq(YhqExchangeRecord record, List<YhqExchangeDetail> details) {
	...
	try {
        ...
		// 生成优惠券信息
		yhqTicketInfoService.productYhqTicketInfo(x, x, x);	
		...		
		// 扣减企业账户锁定金额,这里是出错的地方,是本service的另外一个方法
        updateAccountInfo(x, x,x);
	} catch(BusinessException e) {
		log.error(...);
		throw e;
	} catch(Exception e){
		log.error(...);
		throw new BusinessException(SysErrorConfig.SYS_0000000001);
	}
}

 

兄弟嵌套事物:

@Transactional(value="transactionManagerHx",rollbackFor=Exception.class)
public void updateAccountInfo(String custmoterCode, long amount, String recordId) {
	...
	if (result < 0) {
        log.error(...);
        //程序走到这里,抛出异常
		throw new BusinessException(...);
	} else {
        ...
	}
    ...
}

派发优惠券:

@Transactional(value="transactionManagerHx",rollbackFor=Exception.class)
public void productYhqTicketInfo(x,x, x) {
	// 派发优惠券
    //这里用sqlMapB去操作优惠券
}

---------------------over-------------------------

猜你喜欢

转载自my.oschina.net/u/3844121/blog/1802052