作者简介:
“卡夫卡”,高级软件工程师,2010年正式加入携宁,目前在FIA投研卖方产品组从事投研系统的研发工作。
事情起因
某日A君找到笔者说生产遇到一个问题很奇怪,内层方法报错导致相关一系列操作都回滚。笔者当时回答是内存方法报错异常抛出导致SpringAop捕获到异常因此回滚属于正常现象。
但是A君又说报错的方法内部有try-catch而且并未thorws到外层方法,为什么还是触发回滚?
带着这个问题让我们看看当时究竟发送了什么。
事件还原
外层方法内部包含三个子操作分别为:
• 方法一:查询记录[分支操作],出现异常不能影响主干操作,有try-catch且异常禁止thorw到上层方法。
• 方法二:删除记录[主干操作],必须事务控制。
• 方法三:保存记录[主干操作],必须事务控制。
关键代码如下:
/** * 外层方法,简称outMethod */@Transactional(propagation = Propagation.REQUIRED)public void outMethod(...) { // 查询记录[分支操作] service.innerMethod_1(); // 删除记录[主干操作] service.innerMethod_2(); // 保存记录[主干操作] service.innerMethod_3(); }/** * 内层方法一,简称innerMethod_1 * 查询记录[分支操作],出现异常不能影响主干操作 */public void innerMethod_1(...) { // try-catch包裹整个Method并未抛出异常 try { } catch (...) { }}/** * 内层方法二,简称innerMethod_2 * 批量删除记录[主干操作],必须事务控制 */public void innerMethod_2(...) { //...}/** * 内层方法三,简称innerMethod_3 * 批量保存记录[主干操作],必须事务控制 */public void innerMethod_3(...) { //...}
配置文件:
"transactionManager" "sessionFactory"
运行结果:innerMethod_1()内存查询出现【报错】被该方法内部的try-catch捕获但并没有往上抛出异常。结果innerMethod_2()、innerMethod_3()运行完成之后数据库记录没有改变。
明明已经try-catch且未往上抛出异常,根据Spring事务处理方式不会设置为rollback标志,按理说不影响后续的执行为什么事务回滚了?
Spring事务简介
在此之前让我们看看Spring事务的处理流程@Transaction是如何工作的呢?
下图为Spring事务的时序图:
提示:图中的CglibAopProxy在你的本地代码也可能是由JdkDynamicAopProxy来提供AOP,但这不影响本文中讨论的问题。
我们主要从图中第(4)步看起,也就TransactionInterceptor extends TransactionAspectSupport的内容,这是Spring事务调用的核心。
public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean { protected Object invokeWithinTransaction(Method method, @Nullable Class> targetClass, final InvocationCallback invocation) throws Throwable { TransactionAttributeSource tas = getTransactionAttributeSource(); // 获取对应的事务属性,如果事务属性为空(则目标方法不存在事务) final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); // 根据事务的属性获取beanFactory中的PlatformTransactionManager(接口)的实现类,由XML配置可知这里是HibernateTransactionManager final PlatformTransactionManager tm = determineTransactionManager(txAttr); // 构造方法的唯一标识(class.method,例 com.xxx.xxx.xxxClass.outMethod) final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); // 声明式事务 if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) { /** * 创建 TransactionInfo 这里是事务的重点!!! * 会判断是否存在事务,以及根据事务的传播属性做出不同的处理 * 也是做了一层包装,核心是通过TransactionStatus来判断事务的属性。 */ TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); Object retVal = null; try { // 执行被增强方法,即目标方法(业务逻辑) // 最终是通过AopUtils里面的方法通过反射目标方法 retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) { // 异常回滚 completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { // 清除信息:就是把oldTransactionInfo指向当前线程的transactionInfoHolder // transactionInfoHolder是ThreadLocal cleanupTransactionInfo(txInfo); } // 提交事务 commitTransactionAfterReturning(txInfo); return retVal; } else { // 编程式事务处理逻辑,不是我们今天的重点不做展开 } } /** * 内部类,这是Spring事务关键所在 */ protected final class TransactionInfo { // 事务管理器:上面XML代码明确指出为HibernateTransactionManager private final PlatformTransactionManager transactionManager; // 事务传播,隔离级别 private final TransactionAttribute transactionAttribute; // 连接点识别:例如class.method private final String joinpointIdentification; // 事务状态(重点关注对象) private TransactionStatus transactionStatus; /** * TransactionInfo的数据结构本质是一个单向队列 * oldTransactionInfo指向的是线程中上次(外层)调用的 * 线程调用栈经过几次事务增加就有几个节点 */ private TransactionInfo oldTransactionInfo; }}
▼
Spring事务处理分为声名式事务与编程式事务,对于声名式事务处理步骤如下:
• 获取对应的事务属性
• 获取事务管理
• 获取事务信息
• 执行目标方法
• 异常:回滚事务
• 必然:清理事务信息
• 成功:提交事务
以上是对Spring事务处理的简单回顾。
代码分析
• innerMthod_1()
关键代码如下:
// 异常被捕获且未抛出try { // ... IMetaDBQuery query = this.getMetaDBContext().createSqlQuery(sql); // 1.getResult()后续调用MetaDBHQLQueryImpl.getResult() -> doGetResult()。 List
现在已经找到异常产生的地方以及知晓异常被捕获并抛出了。具体调用链如下所示:
在JdbcResourceLocalTransactionCoordinatorImpl类
的TransactionDriverControlImpl内部类中调用markRollbackOnly方法把rollbackOnly属性设置为true,参见如下代码:
public class JdbcResourceLocalTransactionCoordinatorImpl implements TransactionCoordinator { //... public class TransactionDriverControlImpl implements TransactionDriver { // 默认为false private boolean rollbackOnly = false; // 最后调用此方法修复rollbackOnly的属性的值 public void markRollbackOnly() { //... rollbackOnly = true; } }}
让我们看看这个此时线程中的事务产生了哪些变化?
Spring当前事务存储在名为“Current aspect-driven transaction”的ThreadLocal中。
内外层事务使用了同一个sessionHolder,内层方法修改rollbackOnly=true,则线程中的事务已经被标记回滚。(真相了!)
截止目前已经完成innerMthod_1(),可以看到内层方法异常被捕获并且设置rollbackOnly=true。
• innerMthod_2() & innerMthod_3()
innerMthod_2()删除操作跟innerMthod_3()新增操作都是批量操作,例如:删除2000条,新增3000条记录。
Spring事务基于线程上下文的方式存储每条记录。2000(del) + 3000(add)一共5000个待处理对象。
public abstract class TransactionSynchronizationManager { //... private static final ThreadLocal> synchronizations = new NamedThreadLocal<>("Transaction synchronizations"); //... public static void registerSynchronization(TransactionSynchronization synchronization) throws IllegalStateException { // 每个具体的事务操作会写入到synchronizations中 synchronizations.get().add(synchronization); } //...}
新增/删除/修复事务的底层调用:
// 写入名为“Transaction synchronizations”的ThreadLocal中TransactionSynchronizationManager.registerSynchronization(synchronization);
这两个方法的调用过程没有任何异常,rollback状态也是false。
截止目前已经完成innerMthod_2() 、innerMthod_3()。并没有任何异常。
• outMethod(代理类)
让我们再次回到TransactionInterceptor extends TransactionAspectSupport
public abstract class TransactionAspectSupport { //... protected Object invokeWithinTransaction(Method method, @Nullable Class> targetClass, final InvocationCallback invocation) throws Throwable { if (...) { //... try { // 1.目标方法outMethod执行完成出栈 retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) { completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { // 2.cleanup正常,先不用管 cleanupTransactionInfo(txInfo); } // 3.进入commit,这里才是真正事务提交。 commitTransactionAfterReturning(txInfo); return retVal; } else { final ThrowableHolder throwableHolder = new ThrowableHolder(); //... } }}
invocation.proceedWithInvocation()已经通过,后续的commitTransactionAfterReturning(txInfo)方法这里才是调用事务的关键。让我们看看这里的调用链:
public class JdbcResourceLocalTransactionCoordinatorImpl implements TransactionCoordinator { //... public class TransactionDriverControlImpl implements TransactionDriver { public void commit() { /** * 因为innerMethod_1()执行报错的时候已经将rollbackOnly置为true * 所以在调用commit()的时候判断rollbackOnly=true直接调用rollback() */ if (rollbackOnly) { //... rollback(); //... } } //... } //...}
因为innerMethod_1()执行报错的时候已经将rollbackOnly置为true,所以代理类在调用TransactionDriverControlImpl调用commit()的时候判断rollbackOnly = true直接调用rollback()事务进行回滚,至此完成对代码的分析。
以上是针对本次生产事故的分析内容。
解决方案
如何解决呢?代码如下:
/** * innerMethod_1() * * 添加@Transactional注解,这里可以是REQUIRES_NEW或者NOT_SUPPORTED,设为只读 * 可以避免异常记录导致回滚 */@Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)public void innerMethod1(...) { }
总结
• 在处理事务问题,首先要根据需求区分哪些是主干哪些是分支。分支的失败时候是否能影响主干。
• 需要了解Spring事务的传播机制、隔离级别、线程绑定。
• 了解Spring的事务管理是基于线程。
• 关注AOP的实现方式,CglibAopProxy与JdkDynamicAopProxy以及各自事务失效的情况。