定位:本文深入剖析 traceId 在线程池场景下的跨线程传递机制,涵盖问题背景、核心组件源码解析、三段式传递设计、父子上下文链路、日志关联查询与故障排查。
摘要 :Java 并发编程中,ThreadLocal在线程池复用场景下天然断层,traceId 丢失、串号、日志无法关联是长期困扰开发者的痛点。本文从源码级拆解一套企业级 "捕获 → 恢复 → 清理"三段式透传方案 :通过装饰器闭包机制,在不侵入业务代码的前提下,实现 traceId 在父子、孙层级线程间无感流转。全文深度解析 Context 父子链设计、ContextTaskDecorator 核心实现、MDCTemplate 日志关联技巧 及 SkyWalking 分布式链路无缝集成,并给出完整的 12 文件最小移植清单 与 traceId 丢失/串号/嵌套断裂三级故障排查手册。一文贯通原理、实现、集成、移植、排障全链路,让异步调用追踪有据可查,整套方案可直接落地至任意 Java 项目。线程池架构设计与实践请参阅 企业级 Java 线程池架构全解:从设计理念到生产实践与项目移植。
目录
- 问题背景与设计目标
- 源码文件清单与职责说明
- 核心组件源码解析
- 传递链路全景
- 传递机制详解
- 日志关联查询
- [SkyWalking 集成](#SkyWalking 集成)
- 传递注意事项
- [trace 组件移植指南](#trace 组件移植指南)
- 故障排查
1. 问题背景与设计目标
1.1 问题背景
在 Java 并发编程中,ThreadLocal 是实现线程隔离上下文传递的标准方案。然而在线程池场景 下,ThreadLocal 存在一个致命问题:
线程池中的线程是复用的 。工作线程一旦创建后会被反复使用执行不同任务,
ThreadLocal不会自动从提交任务的线程(父线程)传递到执行任务的工作线程(子线程)。
这导致以下后果:
- traceId 丢失:主线程生成的 traceId 无法传递到子线程,子线程日志中 traceId 为空
- 日志无法关联:无法通过 traceId 将主线程与子线程的日志串联起来,排查问题困难
- 上下文串号风险:线程复用时如果不清理,前一个任务的上下文可能泄漏到后一个任务
1.2 设计目标
| 目标 | 实现方式 |
|---|---|
| traceId 自动透传 | 装饰器模式在任务提交时自动捕获/恢复 traceId,业务代码无感知 |
| 父子链路保留 | 不是简单复制 traceId,而是建立 Context 父子关系链,支持多层级调用链追踪 |
| 无泄漏 | finally 块强制清理 ThreadLocal,防止线程复用导致的上下文泄漏 |
| 异常兜底 | 装饰器中的异常不影响业务执行,catch 后继续 |
| SkyWalking 集成 | 可选集成 SkyWalking APM,实现分布式链路追踪 |
1.3 解决思路
ThreadLocal 不能跨线程传递
↓
但对象的引用可以通过闭包跨线程
↓
在父线程先把上下文保存为普通变量(捕获)
↓
让 lambda 表达式捕获这个变量(闭包携带)
↓
在子线程中恢复写入子线程的 ThreadLocal(恢复)
↓
执行完毕后清理 ThreadLocal(清理)
这就是 "捕获 → 恢复 → 清理"三段式传递机制 的核心思想。
2. 源码文件清单与职责说明
2.1 trace/juc 包(com.xxx.trace.juc)--- 装饰器与线程池集成
| 文件 | 行数 | 职责 | 核心类/接口 |
|---|---|---|---|
| DecoratorThreadPoolExecutor.java | 64 | 装饰器线程池,继承 ThreadPoolExecutor,在 execute() 中调用 TaskDecorator.decorate() 包装任务后再委托父类执行 |
DecoratorThreadPoolExecutor extends ThreadPoolExecutor |
| ContextTaskDecorator.java | 97 | traceId 上下文透传装饰器,三段式传递的核心实现 | ContextTaskDecorator implements TaskDecorator |
| TaskDecorator.java | 11 | 线程任务装饰器接口,定义 decorate(Runnable) 方法 |
TaskDecorator(接口) |
| SkyWalkingUtil.java | 48 | SkyWalking 链路追踪工具,创建/继承/销毁 Span | SkyWalkingUtil |
2.2 trace 包(com.xxx.trace)--- 上下文管理与 MDC 操作
| 文件 | 行数 | 职责 |
|---|---|---|
| Context.java | 44 | trace 上下文模型,持有 traceId、parent(父上下文)、preTraceId、snapshot(SkyWalking 快照)、自定义数据 Map |
| ContextHolder.java | 43 | ThreadLocal 全局上下文持有器,提供 getOrBuildContext() / setContext() / remove() 方法 |
| MDCTemplate.java | 101 | MDC 操作模板,生成/设置 traceId 到 SLF4J MDC,记录 parentTraceId / preTraceId 日志用于跨线程关联查询 |
| MdcInfo.java | 21 | MDC 信息模型,包含 TraceType 数组和是否刷新 traceId 的标志 |
| TraceConfig.java | 17 | 跟踪配置,控制是否禁用 SkyWalking |
| TraceContextWrap.java | 17 | 基于 SkyWalking 的父子线程上下文粘合包装器 |
| MDC.java (注解) | 24 | 方法级注解,声明 TraceType(LOG/TIMER/METRICS/EVENT)和是否刷新 traceId |
| MDCAspect.java | 42 | @MDC 注解的 AOP 切面,拦截标注方法并调用 MDCTemplate.traceMethod() |
| TraceType.java | 7 | 追踪类型枚举:LOG(日志)、TIMER(计时)、METRICS(指标)、EVENT(事件) |
2.3 工具类
| 文件 | 职责 |
|---|---|
| NanoIdUtils.java | traceId 生成工具,使用 NanoId 算法生成短且唯一的 traceId |
3. 核心组件源码解析
3.1 Context --- trace 上下文模型
源码
java
/**
* trace 上下文
*
* @author liteng
*/
@Data
public class Context {
// 自定义数据
private Map<String, Object> data = new HashMap<>();
// 方法开始时间
private Long startTime;
//当前线程的 traceId
private String traceId;
//父线程的上下文(关键!)
private Context parent;
//SkyWalking 快照
private ContextSnapshot snapshot;
//前序调用链路的 traceId
private String preTraceId;
public String getParentTraceId() {
if (parent == null) {
return null;
}
return parent.traceId;
}
public Object get(String key) {
return data.get(key);
}
public void put(String key, Object value) {
data.put(key, value);
}
}
设计要点:
- parent 字段是链式结构 :
Context持有一个parent引用指向父线程的Context,形成链表而非简单复制值 - getParentTraceId() :通过
parent.traceId获取父线程的 traceId - 支持多层级调用链 :
context.getParent().getParentTraceId()可以获取祖父线程的 traceId - preTraceId:记录 MDC 中原有的 traceId(前序调用链路),用于方法嵌套场景
3.2 ContextHolder --- ThreadLocal 全局上下文持有器
源码
java
public class ContextHolder {
private static final ThreadLocal<Context> contextThreadLocalHolder = new ThreadLocal<>();
public static Context getContext() {
return contextThreadLocalHolder.get();
}
public static Context getOrBuildContext() {
Context context = getContext();
if (context != null) {
return context;
}
context = buildAndAddContext();
return context;
}
public static Context buildAndAddContext() {
Context context = new Context();
setContext(context);
return context;
}
public static void setContext(Context input) {
contextThreadLocalHolder.set(input);
}
public static void remove() {
contextThreadLocalHolder.remove();
}
}
设计要点:
getOrBuildContext():获取或创建上下文,避免空指针remove():清理 ThreadLocal,这是防止线程复用泄漏的关键- 线程池场景下,每个工作线程的 ThreadLocal 是独立的,需要显式设置和清理
3.3 ContextTaskDecorator --- traceId 透传装饰器(核心组件)
这是整个 traceId 跨线程传递机制的核心组件 。
源码
java
/**
* traceId 上下文透传装饰器
* 设计目的:
* 1. 在任务提交时 捕获父线程上下文,在子线程执行时恢复,保证跨线程的traceId透传且不发生串号
* 2. 通过装饰器模式 + ThreadLocal复制 解决线程池场景下,ThreadLocal不会自动传递给子线程的问题
*/
@Slf4j
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ContextTaskDecorator implements TaskDecorator {
/**
* 线程池前缀 标识装饰器归属那个线程池
*/
private String operateName;
/**
* 链路追踪配置
*/
private TraceConfig traceConfig;
/**
* 在主线程(父线程)中对任务进行装饰,将父线程的上下文(traceId)放到lambda表达式(未执行)
* 将装饰好的lambda表达式丢到线程池队列,lambda表达式在子线程中执行
* 解决核心问题:
* ThreadLocal不能跨线程传递,但对象的引用可以通过闭包跨线程
* 所以必须在父线程先把上下文保存为普通变量,再让lambda捕获这个变量,最后在子线程中恢复写入子线程的ThreadLocal形成三段式传递:
* 线程上下文传递流程图
*@formatter:off
主线程 ThreadLocal → 局部变量 parentCxt(捕获)
↓ lambda 闭包携带
子线程执行 → before(parentCxt) → 写入子线程 ThreadLocal(恢复)
↓ finally
ContextHolder.remove()(清理)
* @formatter:on
*/
@Override
public Runnable decorate(Runnable runnable) {
// 捕获当前线程(父线程)的上下文
Context parentCxt = ContextHolder.getOrBuildContext();
// 在父线程捕获 SkyWalking 快照,供子线程链路继承
if (isSkyWalkingEnabled()) {
SkyWalkingUtil.addContextSnapshot(parentCxt);
}
//返回包装后的Runnable(此lanbda表达式在子线程执行)
return () -> {
try {
//建立父子线程上下文关系链
before(parentCxt);
//执行业务逻辑
runnable.run();
} finally {
try {
// 先结束子线程 LocalSpan,再清理 ThreadLocal,避免链路残留
if (isSkyWalkingEnabled()) {
SkyWalkingUtil.endSpan();
}
// 清理操作,避免线程池中的线程复用时出现上下文信息泄漏,造成traceId串号
ContextHolder.remove();
} catch (Exception e) {
log.error("finally, enhance fail ", e);
}
}
};
}
/**
* 在任务执行之前,将父线程的上下文设置到执行任务的子线程中
* 设计意图:不是简单地复制traceId,而是建立父子关系链,保留完整的调用层级
* 子线程可通过childCxt.getParentTraceId()获取父线程的traceId,实现多层级调用链路追踪
*
* @param parentCxt {@link Context}
*/
private void before(Context parentCxt) {
try {
// 在任务执行之前,子线程构建自己的上下文
Context childCxt = ContextHolder.getOrBuildContext();
if (parentCxt != null) {
//建立父子上下文关系链
childCxt.setParent(parentCxt);
}
// 子线程恢复父线程 SkyWalking 快照并创建 LocalSpan,实现跨线程链路继承
if (isSkyWalkingEnabled()) {
SkyWalkingUtil.continueSpan(childCxt, buildSpanName(), parentCxt);
}
} catch (Exception e) {
log.error("before, enhance fail ", e);
}
}
/**
* SkyWalking 集成是否开启(由 TraceConfig.disableSkyWalking 控制,默认开启)
*/
private boolean isSkyWalkingEnabled() {
return traceConfig == null || !traceConfig.isDisableSkyWalking();
}
/**
* 构建子线程 Span 名称:{线程池前缀}-execute,无前缀时使用默认名称
*/
private String buildSpanName() {
if (operateName == null || operateName.trim().isEmpty()) {
return "thread-pool-execute";
}
return operateName + "-execute";
}
}
三段式传递流程
主线程 ThreadLocal → 局部变量 parentCxt(捕获)
↓ lambda 闭包携带
子线程执行 → before(parentCxt) → 写入子线程 ThreadLocal(恢复)
↓ finally
ContextHolder.remove()(清理)
逐行解析:
| 步骤 | 代码位置 | 执行线程 | 作用 |
|---|---|---|---|
| ① 捕获 | Context parentCxt = ContextHolder.getOrBuildContext() |
父线程(提交线程) | 获取父线程的 Context 对象,保存为局部变量 |
| ② 恢复 | before(parentCxt) → childCxt.setParent(parentCxt) |
子线程(工作线程) | 在子线程创建/获取 Context,设置 parent 引用 |
| ③ 执行 | runnable.run() |
子线程 | 执行原始业务逻辑,可通过 ContextHolder 获取上下文 |
| ④ 清理 | finally { ContextHolder.remove() } |
子线程 | 清理 ThreadLocal,防止线程复用时上下文泄漏 |
关键设计决策
-
为什么在父线程捕获,而不是子线程?
decorate()是在DecoratorThreadPoolExecutor.execute()中被调用的,而execute()是在提交任务的线程(父线程)中执行的。此时父线程的 ThreadLocal 中有 traceId。如果不在此处捕获,等任务到了子线程的 ThreadLocal 中就没有 traceId 了。
-
为什么用闭包而不是直接传值?
Java 的 lambda 表达式会捕获外部局部变量的引用。
parentCxt作为一个普通的 Java 对象引用,通过闭包从父线程"携带"到子线程。这绕过了 ThreadLocal 不能跨线程传递的限制。
-
为什么建立父子链而不是简单复制 traceId?
如果只是复制 traceId,就无法区分"哪个线程派生了哪个线程"。建立
parent引用链后,子线程可以通过getParentTraceId()获取父线程 traceId,支持多层级调用链追踪。
-
为什么 finally 中要 try-catch?
如果清理操作本身抛出异常,不能影响已经执行完毕的业务逻辑。catch 后记录日志即可。
3.4 TaskDecorator --- 装饰器接口
源码
java
public interface TaskDecorator {
Runnable decorate(Runnable runnable);
}
设计意图 :定义装饰器接口而非直接绑定 ContextTaskDecorator,允许未来扩展其他类型的装饰器(如安全上下文传递、租户上下文传递等)。
3.5 DecoratorThreadPoolExecutor --- 装饰器线程池
源码
java
/**
* 装饰器线程池
* 采用装饰器模式在任务被线程池执行前,插入一段装饰逻辑TaskDecorator(如:traceId透传、MDC上下文传播)
* 让每个提交到线程池的任务自动 "携带" 父线程的上下文信息
*/
@Setter
public class DecoratorThreadPoolExecutor extends ThreadPoolExecutor {
// 任务装饰器,装饰器的注入点,接收原始任务,返回包装后任务,若不设置,走普通线程池逻辑
private TaskDecorator taskDecorator;
/**
* 所有线程池参数直接委托给父类ThreadPoolExecutor
* 自身只负责接收并保存 taskDecorator
*/
public DecoratorThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler reject,
TaskDecorator decorator) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, reject);
taskDecorator = decorator;
}
/**
* 线程执行
* 关键点:
* decorate()是在提交任务的线程(父线程)中执行的,而不是在线程池的工程线程中,
* 这意味着装饰器可以在此处捕获父线程的traceId/MDC上下文,然后返回一个包装任务,该包装任务在工作线程执行时恢复这些上下文
* @formatter:on
*/
@Override
public void execute(@NonNull Runnable command) {
if (taskDecorator != null) {
// 使用 taskDecorator对子线程进行装饰,获取包装后的Runnable
Runnable decorate = taskDecorator.decorate(command);
//线程池执行Runnable,子线程从队列取出lambda并执行
super.execute(decorate);
} else {
super.execute(command);
}
}
}
执行流程:
业务线程提交任务(command)
↓
execute() 被调用
↓
taskDecorator 存在?
├── 是 → command 被 decorate() 包装 → 包装后的 Runnable 传给 super.execute()
└── 否 → command 直接传给 super.execute()(降级为普通线程池)
关键点 :decorate() 是在提交任务的线程(父线程)中执行的,而不是在线程池的工作线程中。这意味着装饰器可以在此处捕获父线程的
traceId/MDC 上下文,然后返回一个包装任务,该包装任务在工作线程执行时恢复这些上下文。
3.6 MDCTemplate --- MDC 操作模板
源码
java
public class MDCTemplate {
private static final Logger log = LoggerFactory.getLogger("MDC");
private static final String MDC_STR = "MDC";
public static <V> V traceMethod(Callable<V> callable, String signatureName, MdcInfo mdcInfo) {
Context context = null;
try {
// 1. 从 MDC 获取 preTraceId(前序调用链路的 traceId)
String preTraceId = MDC.get(MDC_STR);
// 2. 从 ContextHolder 获取/创建 Context
context = ContextHolder.getOrBuildContext();
if (preTraceId != null) {
context.setPreTraceId(preTraceId);
}
// 3. 获取 parentTraceId(来自 ContextTaskDecorator 设置的 parent)
String parentTraceId = context.getParentTraceId();
// 4. 生成 traceId(NanoId),设置到 Context + MDC
String traceId = getOrGeneratorTraceId(mdcInfo, context);
context.setTraceId(traceId);
context.setStartTime(System.currentTimeMillis());
MDC.put(MDC_STR, traceId);
// 5. 记录关联日志
if (parentTraceId != null) {
//线程池内外关联
log.info("{} start,parentTraceId=[{}] ", signatureName, parentTraceId);
}
if (preTraceId != null) {
//线程池内外关联
log.info("{} start,preTraceId=[{}] ", signatureName, preTraceId);
}
// 6. 执行目标方法
return callable.call();
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 7. 清理 MDC上下文
MDC.remove(MDC_STR);
if (null != context) {
if (Arrays.asList(mdcInfo.getTypes()).contains(TraceType.TIMER)) {
long endTime = System.currentTimeMillis();
log.info("{},Exiting, spend time:{} ", signatureName,
(endTime - context.getStartTime()));
}
// 恢复 preTraceId 到 MDC(支持方法嵌套)
if (StringUtils.isNotBlank(context.getPreTraceId())) {
MDC.put(MDC_STR, context.getPreTraceId());
context.setPreTraceId(null);
}
}
}
}
private static String getOrGeneratorTraceId(MdcInfo mdcInfo, Context context) {
String traceId;
if (!mdcInfo.isRefresh()) {
// 不刷新模式:如果 Context 中已有 traceId 则复用
if (Objects.isNull(context.getTraceId())) {
traceId = NanoIdUtils.randomNanoId();
} else {
traceId = context.getTraceId();
}
} else {
// 刷新模式:总是生成新的 traceId
traceId = NanoIdUtils.randomNanoId();
}
return traceId;
}
}
MDCTemplate 在传递链中的角色:
- 主线程入口 :
@MDC注解触发MDCAspect→MDCTemplate.traceMethod()→ 生成 traceId 设置到 MDC + ContextHolder - 子线程入口 :如果子线程的方法也标注了
@MDC,会调用MDCTemplate.traceMethod(),此时context.getParentTraceId()
已经有值(由ContextTaskDecorator.before()设置),记录parentTraceId关联日志
3.7 MDCAspect --- AOP 切面
源码
java
@Aspect
@Component
public class MDCAspect {
private static final Logger log = LoggerFactory.getLogger("MDC");
@Around("@annotation(MDC)")
public Object traceMethod(ProceedingJoinPoint joinPoint, MDC MDC) throws Throwable {
String signatureName = null;
try {
Class<?> targetClass = AopUtils.getTargetClass(joinPoint.getTarget());
String className = targetClass.getSimpleName();
String methodName = joinPoint.getSignature().getName();
signatureName = className + "." + methodName;
} catch (Exception e) {
log.error("traceMethod error", e);
return joinPoint.proceed();
}
return MDCTemplate.traceMethod(() -> {
try {
return joinPoint.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
}
}, signatureName, new MdcInfo(MDC.types(), MDC.refresh()));
}
}
3.8 @MDC 注解与 TraceType追踪类型枚举
MDC注解
java
/**
* MDC 注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MDC {
TraceType[] types() default TraceType.LOG;
boolean refresh() default true;
}
TraceType追踪类型枚举
java
public enum TraceType {
LOG, // 日志:记录出入参
TIMER, // 计时:记录方法耗时
METRICS, // 指标:上报监控指标
EVENT // 事件:记录关键事件
}
使用示例:
java
@MDC(types = {TraceType.TIMER, TraceType.LOG})
public ResponseDTO request(RequestDTO requestDTO) {
// MDCAspect 拦截 → MDCTemplate.traceMethod()
// 1. 生成 traceId,设置到 MDC + ContextHolder
// 2. 记录 parentTraceId / preTraceId 关联日志
// 3. 执行业务方法
// 4. finally: 清理 MDC,记录耗时
}
3.9 NanoIdUtils --- traceId生成工具
源码
java
public final class NanoIdUtils {
public static final SecureRandom DEFAULT_NUMBER_GENERATOR = new SecureRandom();
public static final char[] DEFAULT_ALPHABET = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
public static final int DEFAULT_SIZE = 21;
private NanoIdUtils() {
}
public static String randomNanoId() {
return randomNanoId(DEFAULT_NUMBER_GENERATOR, DEFAULT_ALPHABET, 21);
}
public static String randomNanoId(Random random, char[] alphabet, int size) {
if (random == null) {
throw new IllegalArgumentException("random cannot be null.");
} else if (alphabet == null) {
throw new IllegalArgumentException("alphabet cannot be null.");
} else if (alphabet.length != 0 && alphabet.length < 256) {
if (size <= 0) {
throw new IllegalArgumentException("size must be greater than zero.");
} else {
int mask = (2 << (int)Math.floor(Math.log((alphabet.length - 1)) / Math.log(2.0D))) - 1;
int step = (int)Math.ceil(1.6D * (double)mask * (double)size / (double)alphabet.length);
StringBuilder idBuilder = new StringBuilder();
while(true) {
byte[] bytes = new byte[step];
random.nextBytes(bytes);
for(int i = 0; i < step; ++i) {
int alphabetIndex = bytes[i] & mask;
if (alphabetIndex < alphabet.length) {
idBuilder.append(alphabet[alphabetIndex]);
if (idBuilder.length() == size) {
return idBuilder.toString();
}
}
}
}
}
} else {
throw new IllegalArgumentException("alphabet must contain between 1 and 255 symbols.");
}
}
}
4. 传递链路全景
主线程(HTTP 请求 / XXL-JOB 调度)
│
│ ① @MDC 注解触发 MDCAspect 切面
│ → MDCTemplate.traceMethod()
│ → 生成 traceId(NanoId),设置到 MDC + ContextHolder (ThreadLocal)
│
│ ② 业务代码提交任务到线程池
│ → DecoratorThreadPoolExecutor.execute(command)
│ → ContextTaskDecorator.decorate(command)
│ → ②a 捕获主线程 Context(含 traceId)
│ → ②b 包装为新的 Runnable
│
▼
子线程(线程池工作线程)
│
│ ③ ContextTaskDecorator 包装的 Runnable 执行
│ → before(parentCxt): 获取/创建子线程 Context,设置 parent
│ → ContextHolder.getOrBuildContext() 得到 childCxt
│ → childCxt.setParent(parentCxt)
│
│ ④ 业务逻辑执行
│ → ContextHolder.getContext() 可获取上下文
│ → context.getParentTraceId() 获取父线程 traceId
│ → context.getTraceId() 获取自身 traceId(如需通过 @MDC 生成)
│
│ ⑤ 任务执行完毕
│ → finally: ContextHolder.remove() 清理 ThreadLocal
│ → 防止线程复用时上下文泄漏
│
▼
日志关联查询
│
├── ",start,parentTraceId=[xxx]" → 查找由主线程派生的子线程日志
└── ",start,preTraceId=[xxx]" → 查找前序调用链路日志
5. 传递机制详解
5.1 第一层:主线程 traceId 生成
java
// 方法标注 @MDC 注解
@MDC(types = {TraceType.TIMER, TraceType.LOG})
public ResponseDTO request(RequestDTO requestDTO) {
// MDCAspect 拦截 → MDCTemplate.traceMethod()
// 1. 从 MDC 获取 preTraceId(前序调用链路的 traceId)
// 2. 从 ContextHolder 获取/创建 Context
// 3. 生成 traceId(NanoId),设置到 Context + MDC
// 4. 如果 parentTraceId 不为空,记录 "xxx start,parentTraceId=[yyy]" 日志
// 5. 如果 preTraceId 不为空,记录 "xxx start,preTraceId=[yyy]" 日志
// 6. 执行业务方法
// 7. finally: 清理 MDC,恢复 preTraceId
}
5.2 第二层:ContextTaskDecorator 自动透传
提交线程(父线程) 执行线程(子线程)
│ │
│ ContextHolder.getOrBuildContext()
│ → 获取主线程 Context(含 traceId)
│ │
│ ──── 包装 Runnable ────▶
│ │
│ │ before(parentCxt)
│ │ → childCxt = ContextHolder.getOrBuildContext()
│ │ → childCxt.setParent(parentCxt)
│ │
│ │ runnable.run()
│ │ → 业务逻辑执行
│ │ → ContextHolder.getContext() 获取上下文
│ │
│ │ finally: ContextHolder.remove()
│ │ → 清理 ThreadLocal
闭包捕获机制详解:
java
public Runnable decorate(Runnable runnable) {
// 此处运行在父线程
// parentCxt 是一个普通的 Java 对象引用,保存在局部变量中
Context parentCxt = ContextHolder.getOrBuildContext();
// 返回的 lambda 表达式捕获了 parentCxt 这个局部变量
// 当 lambda 在子线程执行时,parentCxt 引用仍然指向父线程创建的 Context 对象
return () -> {
try {
before(parentCxt); // 子线程通过闭包访问 parentCxt
runnable.run();
} finally {
ContextHolder.remove();
}
};
}
关键原理 :Java lambda 表达式捕获外部局部变量时,实际上是捕获了变量的引用副本。parentCxt 是一个堆上的 Context
对象,引用可以从父线程通过闭包"携带"到子线程,这绕过了 ThreadLocal 不能跨线程传递的限制。
5.3 第三层:嵌套线程池传递
当子线程再次提交任务到另一个线程池时,ContextTaskDecorator 会自动捕获子线程的 Context(已包含 parent 链),形成 *
Context 父子链*:
主线程 Context(traceId=A)
└─▶ 子线程 Context(traceId=B, parent=Context(A))
└─▶ 孙线程 Context(traceId=C, parent=Context(B))
└─▶ context.getParentTraceId() → B
context.getParent().getParentTraceId() → A
这意味着:
- 每层线程池的
ContextTaskDecorator都会自动捕获当前线程的 Context - 子线程的 Context 已经有 parent 链,再次捕获时整个链都会被传递
- 可以通过
parent链追溯完整的调用层级
6. 日志关联查询
6.1 关键字段说明
| 日志关键字段 | 含义 | 查询场景 |
|---|---|---|
traceId |
当前线程的 traceId | 精确查找某个请求链路 |
parentTraceId |
父线程的 traceId | 查找由主线程派生的子线程日志 |
preTraceId |
前序调用链路的 traceId(MDC 中原有的 traceId) | 查找前序调用链路日志 |
6.2 日志格式示例
主线程: SyncSingleService request start,traceId=[abc123]
子线程: BatchProcessor process start,parentTraceId=[abc123]
子线程: BatchProcessor process start,preTraceId=[abc123]
6.3 查询方式
查询时使用关键字 ",start,parentTraceId=[abc123]" 可找到所有由主线程 abc123 派生的子线程日志。
6.4 日志输出位置
关联日志在 MDCTemplate.traceMethod() 中输出:
java
if(parentTraceId !=null){
// 标识线程池子任务与提交线程的关联关系
log.info("{} start,parentTraceId=[{}] ",signatureName, parentTraceId);
}
if(preTraceId !=null){
// 标识同一调用链中前序方法的 traceId 关联
log.info("{} start,preTraceId=[{}] ",signatureName, preTraceId);
}
7. SkyWalking 集成
7.1 集成架构总览
SkyWalkingUtil 是 SkyWalking APM 与线程池体系的桥接工具,解决 SkyWalking Span 无法自动跨线程继承 的问题:ContextManager 的链路上下文(Segment/Span)绑定在线程上,线程池中的工作线程无法感知父线程的链路状态。
工具类通过 "捕获 → 继承 → 销毁"三段式流程 (与 SkyWalking 官方 Cross Thread Propagation 一致),配合 Context.snapshot 字段实现跨线程链路继承:
父线程(提交线程) 子线程(工作线程)
│ │
│ ① addContextSnapshot(context) │
│ → ContextManager.capture() │
│ → 快照存入父 Context.snapshot │
│ │
│ ───── 闭包携带快照 ────▶ │
│ │ ② continueSpan(child, name, parentCxt)
│ │ → ContextManager.continued(snapshot) 恢复父链路
│ │ → ContextManager.createLocalSpan() 创建子 Span
│ │ → capture() 刷新子 Context 快照(支持多层嵌套)
│ │
│ │ ③ 业务逻辑执行(挂载在子 LocalSpan 下)
│ │
│ │ ④ finally: endSpan()
│ │ → ContextManager.stopSpan() 结束子 Span
7.2 方法详解(基于最新源码)
| 方法 | 执行位置 | 功能 | 关键设计 |
|---|---|---|---|
addContextSnapshot(Context) |
父线程 | 捕获当前 SkyWalking Span 快照,绑定到父 Context | 幂等:Context 为空或已有快照时直接返回;无 Agent 时安全降级 |
continueSpan(Context, String, Context) |
子线程 | 恢复父线程快照并创建子 LocalSpan,实现跨线程链路继承 | 先 continued() 恢复、再 createLocalSpan(),顺序不可颠倒;spanName 空时兜底默认名称 |
endSpan() |
子线程 finally | 结束当前子 LocalSpan | isActive() 判断后 stopSpan(),无 Agent 时安全降级 |
源码
java
/**
* SkyWalking 链路上下文 创建 / 继承 / 销毁工具
*
* <p>跨线程链路继承标准流程(与 SkyWalking 官方 Cross Thread Propagation 一致):</p>
* <ol>
* <li>父线程:{@link #addContextSnapshot(Context)} 捕获父线程 Span 快照,绑定到父 Context</li>
* <li>子线程:{@link #continueSpan(Context, String, Context)} 先恢复父线程快照,再创建子 LocalSpan</li>
* <li>子线程 finally:{@link #endSpan()} 结束子 LocalSpan</li>
* </ol>
*
* <p>注意:本工具依赖以 -javaagent 方式启动的 SkyWalking Agent。
* 无 Agent 或 Agent 异常时,所有方法安全降级(记录 debug 日志),不影响业务执行。</p>
*
* @author liteng
*/
@Slf4j
public class SkyWalkingUtil {
/**
* 默认 Span 名称,spanName 为空时兜底
*/
private static final String DEFAULT_SPAN_NAME = "thread-pool-execute";
/**
* 在父线程中调用:捕获当前 SkyWalking Span 快照并绑定到 Context,
* 供子线程通过 continueSpan 恢复,实现跨线程链路继承。
* 幂等操作:Context 为空或已有快照时直接返回。
*
* @param context 父线程 Context
*/
public static void addContextSnapshot(Context context) {
if (context == null || context.getSnapshot() != null) {
return;
}
try {
if (ContextManager.isActive()) {
context.setSnapshot(ContextManager.capture());
}
} catch (Exception e) {
// 无 Agent 或 Agent 未初始化时安全降级,不影响业务
log.debug("capture SkyWalking snapshot failed, skip, context={}", context, e);
}
}
/**
* 在子线程中调用:恢复父线程快照并创建子 LocalSpan,实现跨线程链路继承。
*
* <p>时序说明:必须先 {@link ContextManager#continued(ContextSnapshot)} 恢复父上下文,
* 再 {@link ContextManager#createLocalSpan(String)} 创建子 Span,顺序不可颠倒;
* 否则子 Span 无法挂载到父链路上,将成为孤儿 Span。</p>
*
* @param context 子线程 Context(可空;非空时刷新快照,供更深层子线程继续继承)
* @param spanName 子线程 Span 名称(可空,空时使用默认名称)
* @param parentCxt 父线程 Context(可空;无快照时不建立继承关系,仅创建本地 Span)
*/
public static void continueSpan(Context context, String spanName, Context parentCxt) {
try {
ContextSnapshot parentSnapshot = parentCxt == null ? null : parentCxt.getSnapshot();
// ① 先恢复父线程快照,再创建子 Span(顺序不可颠倒)
if (parentSnapshot != null) {
ContextManager.continued(parentSnapshot);
}
AbstractSpan span = ContextManager.createLocalSpan(isBlank(spanName) ? DEFAULT_SPAN_NAME : spanName);
if (span != null) {
span.setComponent(ComponentsDefine.JDK_THREADING);
}
// ② 恢复后重新捕获当前快照(含父链路信息),刷新子 Context,支持多层线程池嵌套继承
if (context != null) {
context.setSnapshot(ContextManager.capture());
}
} catch (Exception e) {
// 无 Agent 或链路异常时安全降级,不影响业务执行
log.debug("continue SkyWalking span failed, skip, spanName={}", spanName, e);
}
}
/**
* 在子线程 finally 中调用:结束当前 Span。
*/
public static void endSpan() {
try {
if (ContextManager.isActive()) {
ContextManager.stopSpan();
}
} catch (Exception e) {
// 无 Agent 或链路异常时安全降级,不影响业务执行
log.debug("stop SkyWalking span failed, skip", e);
}
}
private static boolean isBlank(String str) {
return str == null || str.trim().isEmpty();
}
}
7.3 跨线程链路继承时序(核心流程)
父线程(提交任务)
│
│ addContextSnapshot(parentCxt)
│ ├─ ContextManager.isActive()? ← 当前是否有活动 Span
│ │ └─ 是 → ContextManager.capture() 捕获快照
│ │ └─ 否 → 跳过(无线索可继承)
│ └─ parentCxt.snapshot = 快照
│
▼
子线程(执行任务)
│
│ continueSpan(childCxt, spanName, parentCxt)
│ ├─ parentCxt.snapshot != null?
│ │ ├─ 是 → ContextManager.continued(snapshot) ← ① 先恢复父链路(顺序关键!)
│ │ └─ 否 → 跳过(无快照不建立继承,仅创建本地 Span)
│ ├─ ContextManager.createLocalSpan(spanName) ← ② 再创建子 Span
│ │ └─ span.setComponent(JDK_THREADING) 标记为线程池执行组件
│ └─ childCxt.snapshot = ContextManager.capture() ← ③ 刷新快照,支持更深层嵌套继承
│
│ 业务逻辑执行(日志、DB 操作等挂载在子 LocalSpan 下)
│
│ finally: endSpan()
│ └─ ContextManager.isActive()?
│ └─ 是 → ContextManager.stopSpan() 结束子 Span
为什么必须先 continued() 再 createLocalSpan()?
SkyWalking 的 createLocalSpan 会将新 Span 挂载到当前 Segment 的当前 Span 之下。如果先创建 Span 再恢复快照,恢复操作会切换 Segment,导致新创建的 Span 变成孤儿 Span ,无法挂载到父链路上。因此顺序不可颠倒:先恢复父上下文(continued),再创建子 Span(createLocalSpan)。
7.4 ContextTaskDecorator 中的集成点
ContextTaskDecorator 已内置 SkyWalking 集成,调用链完整覆盖 提交 → 执行 → 清理 三个阶段:
java
@Override
public Runnable decorate(Runnable runnable) {
// 捕获当前线程(父线程)的上下文
Context parentCxt = ContextHolder.getOrBuildContext();
// ① 父线程捕获 SkyWalking 快照,供子线程链路继承
if (isSkyWalkingEnabled()) {
SkyWalkingUtil.addContextSnapshot(parentCxt);
}
// 返回包装后的 Runnable(此 lambda 表达式在子线程执行)
return () -> {
try {
before(parentCxt);
runnable.run();
} finally {
try {
// ② 先结束子线程 LocalSpan,再清理 ThreadLocal,避免链路残留
if (isSkyWalkingEnabled()) {
SkyWalkingUtil.endSpan();
}
ContextHolder.remove();
} catch (Exception e) {
log.error("finally, enhance fail ", e);
}
}
};
}
private void before(Context parentCxt) {
try {
Context childCxt = ContextHolder.getOrBuildContext();
if (parentCxt != null) {
childCxt.setParent(parentCxt); // 建立 Context 父子关系链
}
// ③ 子线程恢复父线程 SkyWalking 快照并创建 LocalSpan
if (isSkyWalkingEnabled()) {
SkyWalkingUtil.continueSpan(childCxt, buildSpanName(), parentCxt);
}
} catch (Exception e) {
log.error("before, enhance fail ", e);
}
}
/** SkyWalking 集成是否开启(由 TraceConfig.disableSkyWalking 控制,默认开启) */
private boolean isSkyWalkingEnabled() {
return traceConfig == null || !traceConfig.isDisableSkyWalking();
}
/** 构建子线程 Span 名称:{线程池前缀}-execute,无前缀时使用默认名称 */
private String buildSpanName() {
if (operateName == null || operateName.trim().isEmpty()) {
return "thread-pool-execute";
}
return operateName + "-execute";
}
与纯 traceId 传递的对比:
| 阶段 | traceId 传递(无 SkyWalking) | SkyWalking 集成 |
|---|---|---|
| 提交(父线程) | 捕获 parentCxt 存入闭包 |
额外执行 addContextSnapshot(parentCxt) 捕获 Span 快照 |
| 执行(子线程) | childCxt.setParent(parentCxt) |
额外执行 continueSpan():恢复快照 + 创建 LocalSpan |
| 清理(子线程) | ContextHolder.remove() |
先 endSpan() 结束 Span,再清理 ThreadLocal |
7.5 多层嵌套继承
continueSpan 中在创建子 Span 后重新执行 context.setSnapshot(ContextManager.capture()),将包含父链路信息的当前快照 刷新到子 Context。因此当子线程再次向更深的线程池提交任务时,新的装饰器会捕获到带完整链路信息的快照,实现多层线程池嵌套继承:
主线程(Segment A)
└─▶ 子线程 1(LocalSpan B,快照含 A)
└─▶ 孙线程 2(LocalSpan C,快照含 A+B)
└─▶ 曾孙线程 3(LocalSpan D,快照含 A+B+C)
SkyWalking UI 中可以看到完整的线程池调用链路树。
7.6 配置控制与安全降级
配置控制 :TraceConfig.disableSkyWalking 控制 SkyWalking 集成开关,ContextTaskDecorator.isSkyWalkingEnabled() 读取该配置:
java
@Data
public class TraceConfig {
private boolean disableSkyWalking; // true: 禁用 SkyWalking 集成(默认 false,开启)
}
Nacos 中对应配置:
yaml
exchange:
biz:
global:
trace:
disableSkyWalking: false # 默认开启
安全降级机制(生产可用性的关键设计):
| 场景 | 行为 | 影响 |
|---|---|---|
未以 -javaagent 启动(无 Agent) |
方法内部 try-catch,记录 debug 日志后跳过 | 无影响,业务正常执行 |
| Agent 已启动但当前线程无活动 Span(如定时任务线程) | isActive() 为 false,跳过捕获/停止 |
无线索可继承,仅创建本地 Span |
ContextManager 调用异常 |
try-catch 捕获,记录 debug 日志 | 无影响,业务正常执行 |
disableSkyWalking = true |
isSkyWalkingEnabled() 为 false,完全跳过 SkyWalking 调用 |
无影响,仅保留 traceId 日志关联 |
注意 :本工具依赖以
-javaagent方式启动的 SkyWalking Agent。无 Agent 或 Agent 异常时,所有方法安全降级,不影响业务执行。
7.7 使用注意事项
- Span 命名规范 :子线程 Span 名称为
{线程池前缀}-execute(如plan-partition-batch-execute),无前缀时使用默认名thread-pool-execute,便于在 SkyWalking UI 中按线程池维度筛选 - 必须在 finally 中 endSpan :
ContextTaskDecorator已在 finally 中调用endSpan(),确保异常路径下 Span 也能正常结束,防止 Segment 残留 - 先 endSpan 再清理 ThreadLocal:清理顺序不可颠倒,否则会残留未关闭的 Span 状态
- 关闭开关 :如不使用 SkyWalking,将
TraceConfig.disableSkyWalking设为 true,或迁移时移除SkyWalkingUtil、Context.snapshot字段(详见 [§9 trace 组件移植指南](#§9 trace 组件移植指南))
8. 传递注意事项
-
必须使用 ContextTaskDecorator :在构建线程池时传入
taskDecorator,否则 traceId 不会自动透传 -
finally 必须清理 :
ContextTaskDecorator已在finally中调用ContextHolder.remove(),自定义任务不应额外清理 -
嵌套线程池传递 :子线程再次提交任务到另一个线程池时,traceId 会自动通过
Context.parent链传递 -
日志格式规范 :跨线程日志使用
parentTraceId=[xxx]或preTraceId=[xxx]格式输出 -
异常兜底 :
ContextTaskDecorator的before()和finally块都有 try-catch,装饰器异常不影响业务执行 -
@MDC 注解位置 :
@MDC注解应标注在需要追踪的方法上(如 Service 入口方法),AOP 切面会自动拦截 -
refresh 配置 :
@MDC(refresh = true)(默认)每次生成新的 traceId;refresh = false时如果 Context 中已有 traceId 则复用
9. trace 组件移植指南
9.1 需要迁移的文件清单
com.xxx.trace.juc/
├── DecoratorThreadPoolExecutor.java ← 必须迁移
├── ContextTaskDecorator.java ← 必须迁移(核心组件)
├── TaskDecorator.java ← 必须迁移
└── SkyWalkingUtil.java ← 可选迁移(如使用 SkyWalking)
com.xxx.trace/
├── Context.java ← 必须迁移
├── ContextHolder.java ← 必须迁移
├── MDCTemplate.java ← 必须迁移
├── MdcInfo.java ← 必须迁移
├── TraceConfig.java ← 必须迁移
├── annotations/MDC.java ← 必须迁移
├── aspect/MDCAspect.java ← 必须迁移
└── enums/TraceType.java ← 必须迁移
com.xxx.tools/
└── NanoIdUtils.java ← 必须迁移(traceId 生成工具)
9.2 外部依赖
| 依赖 | 用途 | 是否必须 |
|---|---|---|
org.apache.skywalking:apm-agent-core |
SkyWalking 链路追踪 | 否(可选,由 TraceConfig.disableSkyWalking 控制) |
com.alibaba:fastjson |
@JSONField 注解 |
是(或替换为 @JsonIgnore) |
org.projectlombok:lombok |
@Data / @Slf4j / @Builder |
是 |
org.springframework:spring-aop |
AOP 切面支持 | 是 |
org.aspectj:aspectjweaver |
AOP 切面支持 | 是 |
org.slf4j:slf4j-api |
MDC 操作 | 是 |
9.3 迁移步骤
- 复制源码:将上述文件清单中的 .java 文件复制到目标项目的 common 模块
- 适配包名 :将
com.xxx替换为目标项目的包前缀 - 处理 SkyWalking 依赖 :
- 如有 SkyWalking:保留
SkyWalkingUtil - 如无 SkyWalking:删除
SkyWalkingUtil.java,移除Context.snapshot字段和TraceContextWrap.java
- 如有 SkyWalking:保留
- 配置 Spring 扫描 :确保
com.jzsk.trace.aspect.MDCAspect被 Spring 扫描到(@Component+@Aspect) - 集成到线程池 :确保
BaseInfraExecutorFactory.buildThreadPoolExecutor()传入了ContextTaskDecorator实例
9.4 最小可用版本(无 SkyWalking)
如果目标项目不需要 SkyWalking,trace 相关组件需要迁移以下文件:
trace 核心(8 个文件):
Context.java(移除 snapshot 字段)
ContextHolder.java
MDCTemplate.java
MdcInfo.java
TraceConfig.java
annotations/MDC.java
aspect/MDCAspect.java
enums/TraceType.java
装饰器(3 个文件):
DecoratorThreadPoolExecutor.java
ContextTaskDecorator.java
TaskDecorator.java
工具(1 个文件):
NanoIdUtils.java(traceId 生成)
共 12 个文件即可实现完整的 traceId 跨线程透传能力。
如需同时迁移线程池架构组件,请参阅 企业级 Java 线程池架构全解:从设计理念到生产实践与项目移植 的移植指南。合计约 22
个文件可实现完整的线程池管理 + traceId 透传能力。
10. 故障排查
10.1 traceId 丢失
现象:子线程日志中 traceId 为空或与主线程不一致
排查步骤:
- 确认线程池创建方式 :确认线程池是否通过
ThreadPoolExecutorBuilder.build()创建(产出
DecoratorThreadPoolExecutor) - 确认 taskDecorator 注入 :确认构建时传入了
taskDecorator(ContextTaskDecorator实例) - 确认 before 方法执行 :确认
ContextTaskDecorator.decorate()中before()方法正确设置了parent - 确认 finally 清理 :确认
finally中ContextHolder.remove()被执行(检查是否有异常跳过 finally)
解决方式:
- 不使用
new ThreadPoolExecutor(),必须使用ThreadPoolExecutorBuilder - 确保
BaseInfraExecutorFactory.buildThreadPoolExecutor()传入了ContextTaskDecorator
排查代码路径:
BaseInfraExecutorFactory.afterPropertiesSet()
→ buildThreadPoolExecutor(prefix, new ContextTaskDecorator(prefix, getTraceConfig()))
→ ThreadPoolExecutorBuilder.taskDecorator(contextTaskDecorator)
→ build() → DecoratorThreadPoolExecutor(taskDecorator)
→ execute() → taskDecorator.decorate(command)
→ ContextTaskDecorator.decorate()
→ ContextHolder.getOrBuildContext() ← 捕获
→ before(parentCxt) ← 恢复
→ finally: ContextHolder.remove() ← 清理
10.2 traceId 串号
现象:子线程日志中 traceId 与其他请求的 traceId 混淆
排查步骤:
- 确认 finally 清理 :
ContextTaskDecorator的finally块是否正确执行了ContextHolder.remove() - 检查自定义清理 :业务代码是否在
ContextTaskDecorator之外手动清理了 ThreadLocal,导致清理时机错乱 - 检查线程池复用 :确认是否有线程池外的代码直接操作了
ContextHolder
解决方式:
- 不要在业务代码中手动调用
ContextHolder.remove(),清理由ContextTaskDecorator统一管理 - 确认
ContextTaskDecorator的finally块中 try-catch 是否吞掉了异常
10.3 嵌套传递断裂
现象:孙线程日志中无法获取子线程的 traceId
排查步骤:
- 确认嵌套线程池也使用了装饰器 :子线程提交任务到另一个线程池时,该线程池也需要通过
ThreadPoolExecutorBuilder创建并注入
ContextTaskDecorator - 确认 Context 父子链 :检查
before()方法中childCxt.setParent(parentCxt)是否正确执行 - 确认 ContextHolder 状态 :子线程在提交嵌套任务前,
ContextHolder.getContext()不应为 null
解决方式:
- 所有线程池统一通过
BaseInfraExecutorFactory创建,确保都注入了ContextTaskDecorator - 检查
ContextTaskDecorator.before()中的异常日志,确认没有吞掉设置 parent 的异常