暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

一个常见的事务失效案例

蚂蚁的宝藏 2021-01-23
1764

这是一个普通的一天。

我丝滑且迅速的写下这段代码,自信满满的告诉QA,说没问题了,可以测了。(养成自测的良好习惯)


结果...





一看到这个错,我心想不应该啊,肯定是不会忘记开启exposeProxy
这种小失误啊。

不过看到后半段错误提示:"ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context."

立马反应到可能是因为调用事务方法的线程是mq消费线程池里的锅

想起之前领导对我的谆谆教诲,做学问要严谨,一通源码加调试分析,果不其然,就是这个原因,也让我长了教训!!!


注解方式执行事务本质就是 对该类开启代理对象(spring aop 或者 Aspectj-jdk动态代理或者cglib),然后通过调用代理对象,来执行事务方法。

所以如果我们在本类中,调用了事务注解的方法,是不会生效的。

一般有两个解决方案:

  1. 把事务方法单独写到另外的单独类中
  2. 使用 spring提供的 AopContext
    currentProxy()
    ,它可以帮助我们获取到对应的代理类。
/**
 * ThreadLocal holder for AOP proxy associated with this thread.
 * Will contain {@code null} unless the "exposeProxy" property on
 * the controlling proxy configuration has been set to "true".
 * @see ProxyConfig#setExposeProxy
 */
private static final ThreadLocal<Object> currentProxy = new NamedThreadLocal<>("Current AOP proxy");

AopContent
通过ThreadLocal 保存了每个代理对象,所以如果没有开启这个功能或者 当前线程中获取不到 这个代理对象就会抛出 一开始的错误异常。开启这个功能也很简单,在spring boot启动类上加上注解

@EnableAspectJAutoProxy(exposeProxy = true)
 即可。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {

   /**
    * Indicate whether subclass-based (CGLIB) proxies are to be created as opposed
    * to standard Java interface-based proxies. The default is {@code false}.
    */
   boolean proxyTargetClass() default false;

   /**
    * Indicate that the proxy should be exposed by the AOP framework as a {@code ThreadLocal}
    * for retrieval via the {@link org.springframework.aop.framework.AopContext} class.
    * Off by default, i.e. no guarantees that {@code AopContext} access will work.
    * @since 4.3.1
    */
   boolean exposeProxy() default false;

}

如注解描述,proxyTargetClass
来决定是否强制使用cglib来实现aop,而exposeProxy
则是控制是否暴露代理类,如果开启的话,就能使用AopContext
工具在本类中获取到代理对象。

ps: 注解上这两个配置,决定了spring aop自动创建代理器的处理方式,具体可以参考AbstractAdvisorAutoProxy
这个类,

或者这篇博主的文章 https://www.cnblogs.com/foreveravalon/p/8653832.html

但是!
spring为了实现能在本类,准确来说,是为了能在本线程中去获取代理对象。需要在threadLocal
中去维护当前代理类。

那代理类是什么时机去set进去的呢?

通过追踪,currentProxy
set()
方法,在CglibAopProxy
JdkDynamicAopProxy
中都发现了它的使用。并且两个的处理方式是基本一致的。

public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
   Object oldProxy = null;
   boolean setProxyContext = false;
   Object target = null;
   TargetSource targetSource = this.advised.getTargetSource();
   try {
      if (this.advised.exposeProxy) {
         // 如果exposeProxy配置是开启的,在当前线程中保存当前代理名称
         oldProxy = AopContext.setCurrentProxy(proxy);
         setProxyContext = true;
      }
// 切面执行逻辑代码
      ...
      if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
  ...
         retVal = methodProxy.invoke(target, argsToUse);
      }
      else {
         ...
         retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
      }
      retVal = processReturnType(proxy, target, method, retVal);
      return retVal;
   }
   finally {
      if (target != null && !targetSource.isStatic()) {
         targetSource.releaseTarget(target);
      }
      if (setProxyContext) {
         // threadLoacl 里替换成上一个 代理对象的 name
         AopContext.setCurrentProxy(oldProxy);
      }
   }
}

@Nullable
static Object setCurrentProxy(@Nullable Object proxy) {
   Object old = currentProxy.get();
   if (proxy != null) {
      currentProxy.set(proxy);
   }
   else {
      currentProxy.remove();
   }
   return old;
}

总结来说,如果开启了expose proxy
,spring会做如下处理:

  1. 先在ThreadLocal
    中保存当前代理对象A
  2. 然后执行业务逻辑(业务方法可能会调用另一个代理对象B)
  3. 更新当前线程代理对象(更新当前线程代理对象为B)

所以,这个案例的错误原因就是:

因为我们开启了暴露代理配置,当调用接口,使用@PostMapping()Controller
, spring aop会设置当前线程A(tomact 或者undertow 容器的线程)的代理对象为$Controller
,而rmq线程B在执行注解事务方法A时,通过AopContext
去获取线程A中的代理对象,自然是获取不到。

了解原理后,就知道如何对症下药了,两种方式:

  1. 把事务方法抽出去写在一个单独的类中
  2. 使用手动事务。

因为我这个 consumer类业务逻辑已经很单一了,所以写到外面的类,不合适。
所以,我采用了手动事务的方式。

/**
 * 处理退款完成 消息
 * created by mayibz on 2021/1/19
 */
@Service
@Slf4j
public class OSCRefundApplyConsumer extends OrderStateChangeConsumer {
    ...
    @Override
    protected String getActionTag() {
        return actionTag;
    }

    @Override
    protected String getConsumerGroupName() {
        return groupName;
    }

    @Override
    protected void processWithAction(OrderStateChangeContent changeContent) {
        TransactionStatus transaction = dataSourceTransactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
  ...
 // 业务块
            }
            dataSourceTransactionManager.commit(transaction);
        }catch (Exception e) {
            dataSourceTransactionManager.rollback(transaction);
            throw e;
        }
    }

在重写的时候,也顺便调整了rmq使用不同的处理事件作为tag去投递消息,这样我的消费端,逻辑职责更为单一。


总结:

spring 提供的注解事务,虽然方便,但是如果不清楚原理和逻辑,很有可能会埋个大雷。

- 不要直接在本类中"直接"调用注解事务方法

- 不要在多线程下, 使用AopContext.getCurrentProxy()


文章转载自蚂蚁的宝藏,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论