关于在Spring JPA事务状态readonly下进行更新,数据并没有真正执行更新,却返回操作成功且不抛异常的分析

1、问题描述

从关于sping事务不起效问题中,我们能看出事务控制的基本流程,就是拦截器拦截方法的执行,然后对方法的名称和类型到advices中查找是否有匹配的切面逻辑,并组成一个拦截器list,并顺序执行。

在spring中,发现了一个奇怪的问题,就是在设置事务的切面逻辑表达式中设置了readOnly之后,在对应开启了readOnly事务的方法中,执行写入或更新操作,但并没有如所料报异常或者返回更新失败,反而返回的是更新成功。但最可怕的是,数据库并没有更新数据,这样会造成了假更新或假插入的问题。跟踪多次断点之后发现,插入或更新操作都如所料进行了操作,返回的对象也是更新后的成功对象,但实际上此时数据只在客户端内存页中,并没有flush到服务器内存,因此也不会打印对应的SQL语句。

因此希望能够分析出原因,至少在事务的执行过程中,如果事务的状态是readOnly下进行更新或保存时,报异常或者返回更新失败,能告知调用者无法这样调用。

2、源码分析

在调查资料的过程中,发现了一个博主写的非常清晰的笔记,值得学习:

 

 

在源码断点debug后,最终的发现是在开启了readOnly的事务状态下,在执行JPASimpleRepo的doSave的方法中,在save方法后,调用flush方法时进入了拦截器逻辑,但最后并没有执行flush,或者说并没有真正执行sql语句。

以下是核心的invokeWithinTransaction方法全貌

/**
 * General delegate for around-advice-based subclasses, delegating to several other template
 * methods on this class. Able to handle {@link CallbackPreferringPlatformTransactionManager}
 * as well as regular {@link PlatformTransactionManager} implementations and
 * {@link ReactiveTransactionManager} implementations for reactive return types.
 * @param method the Method being invoked
 * @param targetClass the target class that we're invoking the method on
 * @param invocation the callback to use for proceeding with the target invocation
 * @return the return value of the method, if any
 * @throws Throwable propagated from the target invocation
 */
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
      final InvocationCallback invocation) throws Throwable {

   // If the transaction attribute is null, the method is non-transactional.
   TransactionAttributeSource tas = getTransactionAttributeSource();
   final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
   final TransactionManager tm = determineTransactionManager(txAttr);

   if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager) {
      ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
         if (KotlinDetector.isKotlinType(method.getDeclaringClass()) && KotlinDelegate.isSuspend(method)) {
            throw new TransactionUsageException(
                  "Unsupported annotated transaction on suspending function detected: " + method +
                  ". Use TransactionalOperator.transactional extensions instead.");
         }
         ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType());
         if (adapter == null) {
            throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type: " +
                  method.getReturnType());
         }
         return new ReactiveTransactionSupport(adapter);
      });
      return txSupport.invokeWithinTransaction(
            method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
   }

   PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
   final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

   if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
      // Standard transaction demarcation with getTransaction and commit/rollback calls.
      TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

      Object retVal;
      try {
         // This is an around advice: Invoke the next interceptor in the chain.
         // This will normally result in a target object being invoked.
         retVal = invocation.proceedWithInvocation();
      }
      catch (Throwable ex) {
         // target invocation exception
         completeTransactionAfterThrowing(txInfo, ex);
         throw ex;
      }
      finally {
         cleanupTransactionInfo(txInfo);
      }

      if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
         // Set rollback-only in case of Vavr failure matching our rollback rules...
         TransactionStatus status = txInfo.getTransactionStatus();
         if (status != null && txAttr != null) {
            retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
         }
      }

      commitTransactionAfterReturning(txInfo);
      return retVal;
   }

   else {
      Object result;
      final ThrowableHolder throwableHolder = new ThrowableHolder();

      // It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
      try {
         result = ((CallbackPreferringPlatformTransactionManager) ptm).execute(txAttr, status -> {
            TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
            try {
               Object retVal = invocation.proceedWithInvocation();
               if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
                  // Set rollback-only in case of Vavr failure matching our rollback rules...
                  retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
               }
               return retVal;
            }
            catch (Throwable ex) {
               if (txAttr.rollbackOn(ex)) {
                  // A RuntimeException: will lead to a rollback.
                  if (ex instanceof RuntimeException) {
                     throw (RuntimeException) ex;
                  }
                  else {
                     throw new ThrowableHolderException(ex);
                  }
               }
               else {
                  // A normal return value: will lead to a commit.
                  throwableHolder.throwable = ex;
                  return null;
               }
            }
            finally {
               cleanupTransactionInfo(txInfo);
            }
         });
      }
      catch (ThrowableHolderException ex) {
         throw ex.getCause();
      }
      catch (TransactionSystemException ex2) {
         if (throwableHolder.throwable != null) {
            logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
            ex2.initApplicationException(throwableHolder.throwable);
         }
         throw ex2;
      }
      catch (Throwable ex2) {
         if (throwableHolder.throwable != null) {
            logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
         }
         throw ex2;
      }

      // Check result state: It might indicate a Throwable to rethrow.
      if (throwableHolder.throwable != null) {
         throw throwableHolder.throwable;
      }
      return result;
   }
}

以下是自行封装的类org.fsdcic.common.springjpa.service.impl.BaseServiceImpl的save逻辑

private int doSave(RefObject<T> t, StringBuilder sb) {
    int result = -1;
    if (this.isValidEnity((IBaseEntity)t.argvalue, sb)) {
        T tmp = (IBaseEntity)this.getRepository().save(t.argvalue);
        if (tmp != null) {
            //问题就出在此处,getRepository得到的是一个代理对象,在调用flush的时候会经过各种切面逻辑,但最终在readOnly状态下,并没有任何的响应
            this.getRepository().flush();
            t.argvalue = tmp;
            result = 0;
        }
    }

    return result;
}

在em.flush --> org.hibernate.internal.SessionImpl#doFlush方法中,发布了FlushEvent事件。并由org.hibernate.event.internal.DefaultFlushEventListener#onFlush接收,处理到以下三个核心方法时,输出sql

再进一步对图中的三个方法进行分析:

  • flushEeverythingToExecutions:会发布FlushEntityEvent事件,org.hibernate.event.internal.DefaultFlushEntityEventListener#onFlushEntity中处理,在处理过程中会查看是否存在dirty数据,并把需要提交到服务端的数据,逐一放到session的actionQueue中。(关键就在此,判断是否dirty数据时,会判断事务是否readOnly状态,否则不会放入queue中)

 

 

 

  • performExecutions:执行queue中的event(正是由于上一步,因此这里执行了一个空,造成了flush失败的假象)
  • postFlush:在flush后,需要处理一些对象的消除等操作

3、初步结论:

由于JPA会对操作数据进行flush操作,在该过程中调用的接口方法是javax.persistence.EntityManager#flush,该接口方法的默认实现是Hibernate,因此我们看到不管是flushEvent的发布,或是dirty数据的检查,都是hibernate自己的实现,因此若我们对实现进行更改,会造成侵入式的改造,让日后框架的版本迭代带来风险。因此我们应该思考的是,如果在JPA和Spring层面上解决该问题,利用事务切点的方式。

4、最后解决方式:

通过扩展事务配置的切面类:TransactionInterceptor,在关键方法createTransactionIfNecessary进行判断:若当下存在事务,且事务状态为readOnly的情况下,若新建的事务信息状态为非readOnly,则抛出异常。测试成功!完结撒花

(注意ReadOnlyCheckTransactionInterceptor需要配置好切点,以免无法进入逻辑 ,除了切入service类的方法外,repo方法的切入最好也进行设置,能进一步降低假更新操作带来的风险):

public class ReadOnlyCheckTransactionInterceptor extends TransactionInterceptor {

    @Override
    protected TransactionInfo createTransactionIfNecessary(PlatformTransactionManager tm, TransactionAttribute txAttr, String joinpointIdentification) {
        boolean needToCheck = TransactionSynchronizationManager.isActualTransactionActive() && TransactionSynchronizationManager.isCurrentTransactionReadOnly();
        TransactionInfo currentTransactionInfo = super.createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
        if (needToCheck) {
            DefaultTransactionStatus transactionStatus = (DefaultTransactionStatus) (currentTransactionInfo.getTransactionStatus());
            if (!transactionStatus.isNewTransaction() && !transactionStatus.isReadOnly()) {
                String errorMsg = "当前已开启的事务状态为readOnly,现操作沿用已有事务,但操作所定义的事务状态非readOnly!请确认是否在get、find方法中执行数据更新操作";
                ReadOnlyTransactionCheckException ex = new ReadOnlyTransactionCheckException(errorMsg);
//                //回滚当前已经创建的事务
//                completeTransactionAfterThrowing(currentTransactionInfo, ex);
                throw ex;
            }
        }
        return currentTransactionInfo;
    }

题外话:

@FunctionalInterface注解:实际上就是可以接受一个lamda表达式(或function,调用某个具体方法的句柄)

例如在事务核心方法invokeWithinTransaction中的调用例子:

  • 方法的入参类型为一个被FunctionalInterface注解的interface类型,而该接口中也仅有一个方法
  • 调用该方法时,接口类型的入参是一个lamda表达式(function),是一个proceed方法的句柄
  • 在invokeWithinTransaction方法中调用的是接口中的方法,但实际上调用执行的是句柄

实际上这样设计就是体现了interceptor基于代理类的递归循环的高阶版实现:invocation::proceed方法,是一个在主抽象类ReflectiveMethodInvocation定义的proceed方法(这里的invocation也是继承于ReflectiveMethodInvocation的Cglib代理类或JDK代理类,spring中的bean都是代理对象)。同时该类中声明了一个index索引,并掌握着interceptor chain对象,只要proceed方法被调用一次,就会执行++index,并从chain中取下一个interceptor后,调用其invoke方法(把自己作为入参传递入去给inteceptor),让inteceptor去决定是否继续走下去就执行proceed方法,从而实现对方法的逻辑增强。

@Override
@Nullable
public Object invoke(MethodInvocation invocation) throws Throwable {
   .......
   // Adapt to TransactionAspectSupport's invokeWithinTransaction...
   return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
}


@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
      final InvocationCallback invocation) throws Throwable {
          .......
          retVal = invocation.proceedWithInvocation();
      }

//接口名字怎么取都行,这里写的callback就是为了体现语义,回调methodInvocation.proceed方法
@FunctionalInterface
protected interface InvocationCallback {

    //该方法实际上实现了松解耦,只要句柄的返回参数相同以及入参类型和数量相同即可
   @Nullable
   Object proceedWithInvocation() throws Throwable;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值