文章目录
- [1. 前言](#1. 前言)
-
- [1.1 单线程回顾](#1.1 单线程回顾)
- [1.2 多线程的崩溃](#1.2 多线程的崩溃)
- [1.3 问题演示](#1.3 问题演示)
- [2. 解决方案](#2. 解决方案)
-
- [2.1 场景一:手动创建线程](#2.1 场景一:手动创建线程)
- [2.2 场景二:自定义线程池](#2.2 场景二:自定义线程池)
-
- [2.2.1 方式 1 :手动恢复](#2.2.1 方式 1 :手动恢复)
- [2.2.2 方式 2 :自定义 TaskDecorator](#2.2.2 方式 2 :自定义 TaskDecorator)
- [2.3 场景三:@Async 注解](#2.3 场景三:@Async 注解)
- [2.4 场景四:JDK 共享线程池](#2.4 场景四:JDK 共享线程池)
- [2.5 场景五:JDK 原生普通线程池](#2.5 场景五:JDK 原生普通线程池)
- [3. 方案升级:集成 context-propagation 传播库](#3. 方案升级:集成 context-propagation 传播库)
-
- [3.1 基础介绍](#3.1 基础介绍)
- [3.2 场景一/二升级:captureAll → wrap(手动逻辑的标准化版)](#3.2 场景一/二升级:captureAll → wrap(手动逻辑的标准化版))
- [3.3 场景五升级:JDK 原生线程池 ------ ContextExecutorService.wrap(一行)](#3.3 场景五升级:JDK 原生线程池 —— ContextExecutorService.wrap(一行))
- [3.4 场景二升级:TaskDecorator 不用自己写了](#3.4 场景二升级:TaskDecorator 不用自己写了)
- [3.5 场景三解密:@Async 的 propagate-context 底层就是传播库](#3.5 场景三解密:@Async 的 propagate-context 底层就是传播库)
- [3.6 手动 vs 库:什么时候选谁](#3.6 手动 vs 库:什么时候选谁)
1. 前言
1.1 单线程回顾
前面所有案例都是单线程内嵌套调用,底层依靠 OTel 的 ThreadLocal<Context> 自然继承父上下文,实现链路父子关系、串联整个执行链路。
埋点示例:
java
Observation observation = Observation.start("style2.officialManual", registry)
.lowCardinalityKeyValue("style", "2");
try (Observation.Scope scope = observation.openScope()) {
// 执行业务逻辑
} catch (Exception e) {
observation.error(e);
throw e;
} finally {
observation.stop();
}
ThreadLocal是同线程内的上下文容器,嵌套调用天然继承。
1.2 多线程的崩溃
一旦换线程,ThreadLocal 失效:
java
ExecutorService executor = Executors.newFixedThreadPool(4);
Observation observation = Observation.start("style2.officialManual", registry)
.lowCardinalityKeyValue("style", "2");
try (Observation.Scope scope = observation.openScope()) {
// 主线程打开Scope,提交异步任务
executor.submit(() -> {
// 【问题】子线程无有效Trace上下文,拿不到traceId
// 链路上下文丢失!
businessLogic();
});
} catch (Exception e) {
observation.error(e);
throw e;
} finally {
observation.stop();
}
工作线程的 ThreadLocal 是一张白纸,根本就没有父 Span,建出来的是根 Span ,traceId 都和主线程不一样。链路在这里彻底断裂。
1.3 问题演示
改造之前的 OrderService 在下单方法内部调用支付服务:
java
public OrderResult createOrder(String orderNo, Long userId, String orderType) {
OrderObservationContext context = OrderObservationContext.builder()
.operationMetadata(BusinessOperationMetadata.builder()
.operationType(BusinessOperationType.ORDER.value())
.provider("order-service")
.build())
.orderNo(orderNo)
.userId(userId)
.orderType(orderType)
.build();
return OrderObservationDocumentation.ORDER_CREATE
.observation(this.observationConvention.getIfAvailable(), new DefaultOrderObservationConvention(),
() -> context, this.observationRegistry)
.observe(() -> {
// ------ 业务逻辑 ------
// 下单
String orderId = "ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
BigDecimal amount = new BigDecimal("99.90");
context.setOrderId(orderId);
context.setAmount(amount);
context.setStatus("CREATED");
// 支付
this.paymentService.pay(orderId, "WECHAT");
return new OrderResult(orderId, orderNo, amount, "CREATED");
});
}
链路层级:

原始链路层级排版:
java
spring-micrometer-service-aa http get /api/order/create 66.9ms ← 父Span(Root Span)
└── spring-micrometer-service-aa order NORMAL 16.9ms ← 子Span
└── spring-micrometer-service-aa payment WECHAT ← 孙Span
将支付使用异步执行任务:
java
// 支付
new Thread(() -> {
this.paymentService.pay(orderId, "WECHAT");
}).start();
新线程不会继承父线程 ThreadLocal,OTel 链路上下文会丢失,导致异步任务中的支付 Span 变成了根 Span :

2. 解决方案
2.1 场景一:手动创建线程
上面的【问题演示】可以这么改,异步线程外获取当前 Scope 中的观测对象,再手动将其设置到异步现场的 Scope 中:
java
// 捕获当前观测,跨线程恢复 scope(只依赖 micrometer-observation 核心)
Observation orderObservation = observationRegistry.getCurrentObservation();
new Thread(() -> {
assert orderObservation != null;
try (Observation.Scope scope = orderObservation.openScope()) {
this.paymentService.pay(orderId, "WECHAT");
}
}).start();
基本原理:
observe(...)回调执行时,order.create的scope已压在当前线程栈上,所以getCurrentObservation能拿到它。new Thread里orderObservation.openScope()把该观测压到工作线程的scope栈 →paymentService.pay()里observe(...)读到的父级就是它 → 子span挂对。
2.2 场景二:自定义线程池
定义一个支付线程池:
java
private ThreadPoolTaskExecutor buildPaymentExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("order-pay-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
2.2.1 方式 1 :手动恢复
和 new Thread 一样 openScope() 在目标线程恢复:
java
// 支付:线程池异步执行,跨线程恢复观测 scope 以保持链路
Observation orderObservation = observationRegistry.getCurrentObservation();
paymentExecutor.execute(() -> {
assert orderObservation != null;
try (Observation.Scope scope = orderObservation.openScope()) {
this.paymentService.pay(orderId, "WECHAT");
}
});
2.2.2 方式 2 :自定义 TaskDecorator
TaskDecorator 是 Spring 提供的任务边界装饰器接口,用于在任务提交到线程池时做横切处理。
java
@FunctionalInterface
public interface TaskDecorator {
Runnable decorate(Runnable runnable);
}
decorate() 在提交线程捕获当前观测,包一层 Runnable 在工作线程 observation.openScope() 里执行任务:
java
/**
* 提交任务时捕获当前观测,工作线程执行时恢复其 scope,
* 使异步任务正确挂到父观测(链路)下面。只依赖 micrometer-observation 核心。
*/
public class ObservationTaskDecorator implements TaskDecorator {
private final ObservationRegistry observationRegistry;
public ObservationTaskDecorator(ObservationRegistry observationRegistry) {
this.observationRegistry = observationRegistry;
}
@Override
public Runnable decorate(Runnable task) {
Observation.Scope currentScope = this.observationRegistry.getCurrentObservationScope();
Observation observation = (currentScope != null) ? currentScope.getCurrentObservation() : null;
if (observation == null) {
return task;
}
return () -> {
try (Observation.Scope scope = observation.openScope()) {
task.run();
}
};
}
}
ThreadPoolTaskExecutor 配置 ObservationTaskDecorator :
java
private ThreadPoolTaskExecutor buildPaymentExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("order-pay-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.setTaskDecorator(new ObservationTaskDecorator(this.observationRegistry));
executor.initialize();
return executor;
}
调用线程池执行:
java
// 支付:线程池异步执行,TaskDecorator 负责跨线程恢复观测 scope 以保持链路
paymentExecutor.execute(() -> this.paymentService.pay(orderId, "WECHAT"));
2.3 场景三:@Async 注解
如果你使用 @Async 方法且依赖自动配置的 AsyncTaskExecutor,需要通过配置 spring.task.execution.propagate-context=true 主动开启上下文传播。
配置示例:
yml
spring:
task:
execution:
# 开启上下文传播(适配 @Async 线程池)
propagate-context: true
2.4 场景四:JDK 共享线程池
ForkJoinPool.commonPool() :JVM 进程内唯一公共池,不用自己创建,全局所有代码共享。
不指定线程池调用以下方法时触发:
java
// 使用 ForkJoinPool.commonPool()
CompletableFuture.runAsync(() -> {
System.out.println(Thread.currentThread().getName());
});
CompletableFuture.supplyAsync(() -> {
return "test";
});
方式 1 ,手动传播:
java
// ------ 提交线程抓当前观测(此刻父 scope 仍打开),工作线程 openScope() 恢复,
// 不依赖 TaskDecorator,对 commonPool 同样生效,但每处异步都要自己写一遍
Observation orderObservation = observationRegistry.getCurrentObservation();
CompletableFuture.runAsync(() -> {
if (orderObservation == null) {
this.paymentService.pay(orderId, "WECHAT"); // 无观测时直接执行,不传播
return;
}
try (Observation.Scope scope = orderObservation.openScope()) {
this.paymentService.pay(orderId, "WECHAT");
}
});
方式 2 ,显式传带 TaskDecorator 的线程池:
java
// 显式传带 TaskDecorator 的线程池 ------ scope 被恢复,Trace 保持父子关系
CompletableFuture.runAsync(() -> this.paymentService.pay(orderId, "WECHAT"), this.paymentExecutor);
2.5 场景五:JDK 原生普通线程池
常见创建方式:
java
Executors.newFixedThreadPool()
Executors.newCachedThreadPool()
Executors.newSingleThreadExecutor()
JDK 原生的 ThreadPoolExecutor/ExecutorService 没有 setTaskDecorator,那是 Spring ThreadPoolTaskExecutor 独有的能力。只能靠提交时手动 getCurrentObservation() + 工作线程 openScope() 方式:
java
// 三种常见创建方式(JDK 原生 ThreadPoolExecutor,均无 setTaskDecorator)
ExecutorService fixed = Executors.newFixedThreadPool(2); // 固定 2 线程,无界队列
ExecutorService cached = Executors.newCachedThreadPool(); // 按需创建,空闲 60s 回收
ExecutorService single = Executors.newSingleThreadExecutor(); // 单线程串行
// 原生池不传播观测 scope,只能手动恢复(同方式 C)
Observation obs = observationRegistry.getCurrentObservation();
Runnable payTask = () -> {
if (obs == null) {
this.paymentService.pay(orderId, "WECHAT");
return;
}
try (Observation.Scope scope = obs.openScope()) {
this.paymentService.pay(orderId, "WECHAT");
}
};
CompletableFuture.runAsync(payTask, fixed);
CompletableFuture.runAsync(payTask, cached);
CompletableFuture.runAsync(payTask, single);
// 提交完立即 orderly shutdown:已提交任务仍会执行,池资源被回收(避免演示端点反复调用泄漏线程)
fixed.shutdown();
cached.shutdown();
single.shutdown();
3. 方案升级:集成 context-propagation 传播库
3.1 基础介绍
上下文搬运是一个横切关注点 。手写是"点对点"方案,需要一种"面"上的统一机制,这就是 Micrometer 生态的 context-propagation 库(Spring、Micrometer、Reactor 三团队共同设计,Micrometer 1.10 / Reactor 3.5 起被框架内置支持)。
核心概念:
| 概念 | 作用 | 类比手写方案 |
|---|---|---|
ContextRegistry |
注册中心(单例),登记"有哪些上下文" | ------ |
ThreadLocalAccessor |
每种上下文的适配器(读写 ThreadLocal 的契约) | ------ |
ContextSnapshotFactory.captureAll() |
拍快照:一次带走全部已注册上下文 | ≈ getCurrentObservation() |
ContextSnapshot.wrap(runnable) |
包装任务:执行前还原、执行后还原现场 | ≈ openScope() 的标准化版 |
Observation 的适配器不用自己写,micrometer-observation 内置了 ObservationThreadLocalAccessor(key = "micrometer.observation"),并经 SPI(jar 内 META-INF/services/io.micrometer.context.ThreadLocalAccessor)自动注册进全局 ContextRegistry。同理 Slf4jThreadLocalAccessor 负责 MDC。
就是说:前文手写版的"捕获"逻辑,库用 captureAll() 一行替代,而且捕获的是全部已注册上下文,不只是 Observation 一个。
3.2 场景一/二升级:captureAll → wrap(手动逻辑的标准化版)
java
// 快照工厂:每个工程一个即可
ContextSnapshotFactory factory = ContextSnapshotFactory.builder().build();
// ① 捕获:提交前调用(此时父 scope 仍打开)------等价于手写版的 getCurrentObservation()
ContextSnapshot snapshot = factory.captureAll();
// ② 传播:等价于手写版的 openScope(),但判空、还原全部内置
new Thread(snapshot.wrap(() -> this.paymentService.pay(orderId, "WECHAT"))).start();
paymentExecutor.execute(snapshot.wrap(() -> this.paymentService.pay(orderId, "WECHAT")));
和 2.1 / 2.2.1 手写 8 行对比,差异在两点:
- 判空被消化掉了 :手写版
getCurrentObservation()可能为null要自己分支;captureAll()永远返回快照------线程上没有上下文时就是空快照,wrap后原样执行,无副作用; - 还原是库保证的 :
wrap内部是"执行前把快照值set进ThreadLocal→try-with-resources→ 执行后还原工作线程原状态",不会出现忘了finally关Scope的残留。
3.3 场景五升级:JDK 原生线程池 ------ ContextExecutorService.wrap(一行)
2.5 的结论是"JDK 原生池没有 setTaskDecorator,只能手动恢复"------有了库,这条结论可以升级:
java
// 直接把 JDK 原生池包一层:提交时自动捕获、执行时自动还原
ExecutorService wrapped = ContextExecutorService.wrap(Executors.newFixedThreadPool(2), factory);
wrapped.execute(() -> this.paymentService.pay(orderId, "WECHAT"));
newFixedThreadPool / newCachedThreadPool / newSingleThreadExecutor 全部适用,业务代码零侵入------和 TaskDecorator 是同一个思路,区别是它是 ExecutorService 包装器、不依赖 Spring ,纯 JDK 工程也能用。
场景四的 ForkJoinPool.commonPool() 不是 ExecutorService,用 wrap 包任务即可:
java
CompletableFuture.runAsync(snapshot.wrap(() -> this.paymentService.pay(orderId, "WECHAT")));
3.4 场景二升级:TaskDecorator 不用自己写了
2.2.2 手写的 ObservationTaskDecorator 只传 Observation 一种上下文。Spring 6.1 内置了 ContextPropagatingTaskDecorator ,它的核心逻辑就是一行------factory.captureAll().wrap(task),Observation + MDC + 其他已注册上下文一次全带走:
java
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 手写的 ObservationTaskDecorator 类直接删掉,换 Spring 内置的
executor.setTaskDecorator(new ContextPropagatingTaskDecorator());
| 2.2.2 手写 decorator | Spring 内置 decorator | |
|---|---|---|
| 传播的上下文 | 只有 Observation | 全部已注册(Observation + MDC + 其他) |
| 判空 / 还原 | 自己写 | captureAll().wrap() 内置 |
| 代码量 | ~20 行 | 1 行配置 |
3.5 场景三解密:@Async 的 propagate-context 底层就是传播库
2.3 的 spring.task.execution.propagate-context: true 是"配置一行就全自动"------它的底层正是 3.5 的 ContextPropagatingTaskDecorator:Boot 自动为自动配置的 AsyncTaskExecutor 挂上这个 decorator,本质就是 captureAll().wrap()。
也就是说:2.3 已经在用传播库了,只是 Boot 替你接线。两个版本注脚:
- 该配置由 S
pring Boot 3.1引入,早期默认false,需要显式开启(2.3的写法正确); - Spring Boot 3.2 起默认值改为
true,新工程即使什么都不配,@Async的上下文传播也是开着的。
3.6 手动 vs 库:什么时候选谁
前文手写方案不是白写的------它是理解库的基础,也是某些场景的合理选择:
- 选手写:上下文只有一两种、异步调用点就三五个、或想避免多一个依赖------手写透明直白,反而好维护;
- 选库 :上下文多种并存(
Observation+MDC+ 安全上下文 + 租户)、异步边界多、或将来要碰Reactor/WebFlux------库的快照"一次全量带走",新加上下文只改注册表,已有代码零改动。
两者不是对立的:库的 wrap() 内部执行的就是手写那套流程,只是把最容易写错的"判空、开闭、还原"标准化了。判断规则一句话:上下文种类 × 调用点规模,乘积越大,库的收益越明显。