这是近两天实际遇到的业务场景,难得有机会将spring事务管理的知识与实际业务场景相结合,产生一定的业务价值,遂记录。以下会直接给出解决方案,然后给出使用事务管理时的注意点。
您需了解:spring事务管理详细介绍与简单实例
业务场景:按照客户的维度去同步Amazon平台订单(by calling amazon mws api)。
spring事务管理器的配置:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd">
<context:component-scan base-package="com.best.global.glink.dao"/>
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="java:comp/env/jdbc/glink"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/>
</bean>
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory"/>
<constructor-arg index="1" value="REUSE"/>
</bean>
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="txManager"/>
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="txPointcut"
expression="(execution (* com.best.global.glink.service..*.*(..))) || (execution (* com.best.global.glink.facade..*.*(..)))"/>
<aop:advisor pointcut-ref="txPointcut" advice-ref="txAdvice"/>
</aop:config>
</beans>
由此配置可以看出,service及facade层通过spring aop的方式开启了默认事务,且事务传播类型为REQUIRED(该事务类型表示,如果当前存在事务则使用当前已存在的事务,否则新开启一个事务),事务管理器使用JDBC的事务管理器(这一点很重要,因为它可以使用NESTED级别的事务传播行为)。
修改前:
同步方式:所有客户均在一个事务中去完成同步操作(同一个service),代码如下:
@Override
public Boolean syncAmazonOrders(Date fromTime, Date toTime) throws DatatypeConfigurationException {
if (fromTime == null || toTime == null || toTime.before(fromTime)) {
BizLogger.syserror("From time: " + fromTime + ", to time: " + toTime);
return false;
}
//so far, we just have Amazon sales channel, so all means amazon.
List<CustomerSalesChannelVO> channels = customerService.getAllCustomerSalesChannel();
if (channels == null) {
return false;
}
for (CustomerSalesChannelVO channel : channels) {
logger.info("Customer info: {}.", channel);
try {
syncAmazonOrders(channel, fromTime, toTime);
} catch (Exception e) {
BizLogger.syserror(String.format("Amazon order sync error, CustomerCode : %s", channel.getCustomerCode()), e);
}
}
return true;
}
缺点:不同客户订单的同步会彼此影响,实际遇到最多的情况是:部分客户的amazon mws api验证信息更新不及时导致调用api时出错,事务被标记为rollbackonly。
修改后:
由于service层有默认的事务,且amazon order的同步是有Job触发,因此将上述syncAmazonOrders方法中的循环外移即可实现多事务同步不同客户的订单。
@Override
public ProcessResult execute(BestJobAdapter jobAdapter) throws Exception {
/************
/*other codes
/************
for (CustomerSalesChannelVO channel : channels) {
doSyncAmazonOrders(channel, fromTime, toTime);
}
return new ProcessResult(result, "Successfully sync amazon order.");
}
private void doSyncAmazonOrders(CustomerSalesChannelVO channel, Date fromTime, Date toTime) {
logger.info("Customer info: {}.", channel);
try {
amazonOrderService.syncAmazonOrders(channel, fromTime, toTime);
} catch (Exception e) {
BizLogger.syserror(String.format("Amazon order sync error, CustomerCode : %s", channel.getCustomerCode()), e);
}
}
实际测试通过!
以下再谈谈在调整过程中尝试的一些其他做法及存在的问题:
在syncAmazonOrders方法所在类中添加方法手工开启事务:
/**
* 此处事务不生效
* 目前事务切点为service层,所以syncAmazonOrders本身带有事务
* 此处doSyncAmazonOrders开启嵌套事务,事务传播级别Propagation.NESTED,子事务的失败不会影响到外部事务的整体提交,外部事务的失败会回滚所有子事务
* 此处其实最好能够使用外部事务与内部事务互不影响的事务传播级别PROPAGATION_REQUIRES_NEW,但其需要使用JtaTransactionManager作为事务管理器,而我们目前使用的是DataSourceTransactionManager
* 此处由于按照客户的维度已经区分开数据,因此不同的事务不会操作到相同的数据,不需要设置事务的隔离级别
*/
@Transactional(propagation = Propagation.NESTED)
private void doSyncAmazonOrders(CustomerSalesChannelVO channel, Date fromTime, Date toTime) {
logger.info("Customer info: {}.", channel);
try {
syncAmazonOrders(channel, fromTime, toTime);
} catch (Exception e) {
BizLogger.syserror(String.format("Amazon order sync error, CustomerCode : %s", channel.getCustomerCode()), e);
}
}
此处还洋洋洒洒地写了一大片注释,最后发现这里的事务并没有生效。
原因:JDK内置的Proxy动态代理可以在运行时动态生成字节码,而没必要针对每个类编写代理类。中间主要使用到了一个接口InvocationHandler与Proxy.newProxyInstance静态方法,参数说明如下:使用内置的Proxy实现动态代理有一个问题:被代理的类必须实现接口,未实现接口则没办法完成动态代理。这里在同一个类里不同方法的相互调用,被调用的方法是不会开启新事物的。
拓展:
Spring的两种代理JDK和CGLIB的区别:
java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
1、如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
2、如果目标对象实现了接口,可以强制使用CGLIB实现AOP
3、如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换
如何强制使用CGLIB实现AOP?
(1)添加CGLIB库,SPRING_HOME/cglib/*.jar
(2)在spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class="true"/>
JDK动态代理和CGLIB字节码生成的区别?
(1)JDK动态代理只能对实现了接口的类生成代理,而不能针对类
(2)CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法
因为是继承,所以该类或方法最好不要声明成final
参考资料:Spring的两种代理JDK和CGLIB的区别浅谈