前些天发现线上友商企业账户余额不足,只剩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-------------------------