事务里面捕获异常_技术篇 | 记事务回滚问题分析

事务里面捕获异常_技术篇 | 记事务回滚问题分析_第1张图片

作者简介:

“卡夫卡”,高级软件工程师,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事务的时序图:

事务里面捕获异常_技术篇 | 记事务回滚问题分析_第2张图片

提示:图中的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> results = query.getResult();  // ...} catch {} // 2.MetaDBHQLQueryImpl.doGetResult()查询使用getHibernateTemplate()方法。this.getHibernateTemplate().execute(new HibernateCallback() {  query.list();}public List list() {  beforeQuery();  try {    //3.查询方法(发送异常捕获并抛出)    return doList();  } catch (QueryExecutionRequestException he) {    throw new IllegalStateException( he );  } catch (TypeMismatchException e) {    throw new IllegalArgumentException( e );  } catch (HibernateException he) {    // 这里捕获到异常并且往上抛出    throw getExceptionConverter().convert( he );  } finally {    afterQuery();  }}

     现在已经找到异常产生的地方以及知晓异常被捕获并抛出了。具体调用链如下所示:

事务里面捕获异常_技术篇 | 记事务回滚问题分析_第3张图片

       在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;    }  }}

    让我们看看这个此时线程中的事务产生了哪些变化?

事务里面捕获异常_技术篇 | 记事务回滚问题分析_第4张图片

    Spring当前事务存储在名为“Current aspect-driven transaction”的ThreadLocal中。

事务里面捕获异常_技术篇 | 记事务回滚问题分析_第5张图片

    内外层事务使用了同一个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)方法这里才是调用事务的关键。让我们看看这里的调用链:

事务里面捕获异常_技术篇 | 记事务回滚问题分析_第6张图片
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以及各自事务失效的情况。

事务里面捕获异常_技术篇 | 记事务回滚问题分析_第7张图片

你可能感兴趣的:(事务里面捕获异常)