小心 transmittable-thread-local 的这个坑
你好,我是看山。
transmittable-thread-local 是阿里开源一个线程池复用场景下,处理异步执行时上下文传递数据问题的解决方案。可以从官方文档https://github.com/alibaba/transmittable-thread-local获取更多信息。
本文主要是变更 transmittable-thread-local 使用方式时出现的一个异常。
异常现场
看异常之前,先简单说下项目大概情况。
项目是 Java 栈,使用了 SpringBoot+MyBatis 的框架结构,构建工具是 Maven。因为项目中使用了比较多的多线程逻辑,所以引入了 transmittable-thread-local,解决上下文传递数据问题。后来做项目升级,接入公司的监控系统,启动时增加了启动参数-javaagent:/path/to/transmittable-thread-local-2.12.1.jar
,通过零侵入的方式解决多线程上下文传值问题。
于是,有些逻辑出错了。
我们看看异常栈(日志做了删改,隐藏项目信息):
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor' available: expected single matching bean but found 3: executor1,executor2,executor3at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1200)at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:420)at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:349)at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1127)……at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771)at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)……
异常日志很清楚,就是通过AbstractApplicationContext.getBean
获取 Bean 的时候,因为存在多个同类型的ThreadPoolTaskExecutor
,Spring 容器不知道返回哪个 Bean,就抛出了NoUniqueBeanDefinitionException
异常。
排查问题
我们再来看看调用代码:
public static void doSth(Object subtag, Object extra, long time) {ApplicationContextContainer.getBean(ThreadPoolTaskExecutor.class).execute(() -> {// 一些业务代码});
}@Component
public class ApplicationContextContainer implements ApplicationContextAware {private static ApplicationContext applicationContext;public static <T> T getBean(Class<T> clazz) {return applicationContext.getBean(clazz);}@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {ApplicationContextContainer.applicationContext = applicationContext;}
}
可以看出来,applicationContext.getBean
时只传入了 class 类型,没有指明 Bean 的名字。推测是项目中定义了多个ThreadPoolTaskExecutor
类型的 Bean,名字分别是 executor1、executor2、executor3(名字改过了,大家写代码时尽量使用见名知意的起名方式)。
@Configuration
public class ExecutorConfig {@Bean(value = "executor1")public Executor executor1() {ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();// 一些初始化方法taskExecutor.initialize();return taskExecutor;}@Bean(value = "executor2")public Executor executor2() {ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();// 一些初始化方法taskExecutor.initialize();return TtlExecutors.getTtlExecutor(taskExecutor);}@Bean(value = "executor3")public Executor executor3() {ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();// 一些初始化方法taskExecutor.initialize();return TtlExecutors.getTtlExecutor(taskExecutor);}
}
从上面的代码可以发现,确实有 executor1、executor2、executor3 三个Executor
,executor1 是ThreadPoolTaskExecutor
类型的,executor2 和 executor3 是经过TtlExecutors.getTtlExecutor
包装的ThreadPoolTaskExecutor
。
我们来看看TtlExecutors.getTtlExecutor
方法:
public static Executor getTtlExecutor(@Nullable Executor executor) {if (TtlAgent.isTtlAgentLoaded() || null == executor || executor instanceof TtlEnhanced) {return executor;}return new ExecutorTtlWrapper(executor, true);
}
根据错误反推,经过TtlExecutors.getTtlExecutor
之后返回的还是ThreadPoolTaskExecutor
类型。也就是上面代码走了if
语句,直接返回了输入参数。
但是,这里就碰到了两个开发十大未解之谜中的两个:
- 代码没改,之前好好地,怎么就报错了;
- 本地好使,为什么放在服务器上就报错了。
定位问题
首先,我们需要知道,代码的终点不是玄学。我们现在用的计算机还不会撒谎,只要报错了,就一定是有问题。
我们仔细看看TtlExecutors.getTtlExecutor
方法中的if
判断:
- TtlAgent.isTtlAgentLoaded():这个是判断 ttlAgentLoaded 标识,这个后文再说;
- null == executor:输入参数为 null,显然不符合;
- executor instanceof TtlEnhanced:输入参数是
TtlEnhanced
类型,输入的是ThreadPoolTaskExecutor
类型,不符合。
所以,重点看看 ttlAgentLoaded 标识:
public static boolean isTtlAgentLoaded() {return ttlAgentLoaded;
}
从全局找到修改ttlAgentLoaded
的地方是:
public final class TtlAgent {public static void premain(final String agentArgs, @NonNull final Instrumentation inst) {kvs = splitCommaColonStringToKV(agentArgs);Logger.setLoggerImplType(getLogImplTypeFromAgentArgs(kvs));final Logger logger = Logger.getLogger(TtlAgent.class);try {logger.info("[TtlAgent.premain] begin, agentArgs: " + agentArgs + ", Instrumentation: " + inst);final boolean disableInheritableForThreadPool = isDisableInheritableForThreadPool();// 省略非相关代码ttlAgentLoaded = true;} catch (Exception e) {String msg = "Fail to load TtlAgent , cause: " + e.toString();logger.log(Level.SEVERE, msg, e);throw new IllegalStateException(msg, e);}}// 省略非相关代码
}
有一定 javaagent 知识的应该知道,premain
方法是 java 启动时,加载 javaagent 后执行的方法。
这就吻合了。
报错之前的服务器代码,ExecutorConfig
类中定义的 executor1 是ThreadPoolTaskExecutor
类型,executor2 和 executor3 是ExecutorTtlWrapper
类型,使用applicationContext.getBean(clazz)
能够得到名字是 executor1 的 Bean。
然后使用-javaagent:/path/to/transmittable-thread-local-2.12.1.jar
方式实现零侵入的transmittable-thread-local
注入能力。ExecutorConfig
类中定义的 executor2 和 executor3 是ThreadPoolTaskExecutor
类型,使用applicationContext.getBean(clazz)
就会查到三个ThreadPoolTaskExecutor
类型的 Bean,Spring 容器没有办法判断返回哪一个,于是抛出了NoUniqueBeanDefinitionException
异常。
本地启动是加上-javaagent:/path/to/transmittable-thread-local-2.12.1.jar
命令,问题复现。
解决问题
解决上面的报错比较简单,就是使用applicationContext.getBean(beanName, clazz)
方法,通过输入指定的 Bean 的名字和类型,获取确定 Bean,代码修改为:
public static void doSth(Object subtag, Object extra, long time) {ApplicationContextContainer.getBean("executor1", ThreadPoolTaskExecutor.class).execute(() -> {// 一些业务代码});
}
流水线发版回归测试,问题解决。
青山不改,绿水长流,我们下次见。
你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得。
个人主页:https://www.howardliu.cn
个人博文:小心 transmittable-thread-local 的这个坑
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:小心 transmittable-thread-local 的这个坑
C++11中的thread_local是C++存储期的一种,属于线程存储期.存储期定义C++程序中变量/函数的范围(可见性)和生命周期.C++程序中可用的存储期包括auto.register.st ... TLAB是虚拟机在堆内存的eden划分出来的一块专用空间,是线程专属的.在虚拟机的TLAB功能启动的情况下,在线程初始化时,虚拟机会为每个线程分配一块TLAB空间,只给当前线程使用,这样每个线程都单独 ... 最近在看java性能相关方面的书籍.然后在GC调优相关的部分出现了,线程本地分配缓冲区的名词,对于它的调优级为重要,所以就梳理一下这个到底是什么?为什么他对于JVM性能如此重要. 什么是JVM线程本地 ... 写在开头: 我是「猿码天地」,一个热爱技术.热爱编程的IT猿.技术是开源的,知识是共享的! 写博客是对自己学习的总结和记录,如果您对Java.分布式.微服务.中间件.Spring Boot.Sprin ... 有时会需要这种模式,一个全局变量,需要在程序的任何地方都可以使用它,但是当这个变量出现在不同线程时,就要求系统将这个变量拷贝到各个线程中,这样的话,每个线程内部也可以随时访问本线程的全局变量,但是线程 ... (给数据分析与开发加星标,提升数据技能) 来源:jiaxin_12 https://www.cnblogs.com/YangJiaXin/p/11234591.html 背景 测试mysql5.7和m ... 场景: 1. 需要统计某个线程的对象上创建的个数. 2. 当创建的堆空间需要根据线程需要创建和结束时销毁时. 3. 因为范围是线程只能看到自己的存储数据,所以不需要临界区或互斥量来维护自己的堆内存. ... 摘要: 继Ian Goodfellow的推特小课堂之后,特斯拉的人工智能研究负责人.李飞飞斯坦福高徒Andrej Karpathy也在twitter上分享了他对神经网络的一些研究技巧. 继Ian Go ... 向AI转型的程序员都关注了这个号 Netty中的那些坑(上篇) 最近开发了一个纯异步的redis客户端,算是比较深入的使用了一把netty.在使用过程中一边优化,一边解决各种坑.儿这些坑大部分基本上是Netty4对Netty3的改进部 ...小心 transmittable-thread-local 的这个坑相关推荐
最新文章
热门文章