阿里TTL框架
问题背景
在现代微服务架构中,我们经常需要:
- 保留原始请求的认证信息(JWT Token、用户身份等)
- 进行日志链路追踪(TraceId、RequestId等)
- 异步处理长流程(发送邮件、生成报表等)
- 跨微服务调用时转发请求头(Feign调用)
比如:用户注册时,需要异步发送确认邮件,同时调用其他微服务记录日志。这个过程需要保留原始请求的用户身份信息。
Spring原生RequestContextHolder的局限
什么是RequestContextHolder?
RequestContextHolder 是Spring提供的工具类,用来获取当前HTTP请求的上下文信息:
// 同步请求中的使用(✅ 正常工作)
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
String userId = request.getHeader("X-User-ID"); // ✅ 能获取到它是如何工作的?
Spring使用 ThreadLocal 存储请求上下文:
Web容器线程池
│
├─ Thread-1(Tomcat线程)
│ ├─ ThreadLocal<RequestContextHolder>
│ └─ 存储当前HTTP请求信息
│
├─ Thread-2(Tomcat线程)
│ ├─ ThreadLocal<RequestContextHolder>
│ └─ 存储当前HTTP请求信息
│
└─ Thread-3(Tomcat线程)
├─ ThreadLocal<RequestContextHolder>
└─ 存储当前HTTP请求信息❌ 为什么不能直接在异步线程中使用?
这是关键问题!让我们看一个失败的例子:
@PostMapping("/register")
public Result register(User user) {
// 当前线程:Tomcat线程(主线程)
// RequestContextHolder.getRequestAttributes() ✅ 有值
// ❌ 错误做法:直接在异步线程中使用
asyncExecutor.execute(() -> {
// 当前线程:线程池中的新线程(不是Tomcat线程)
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
if (attributes == null) { // 🔴 这里返回null!
// 原因:新线程的ThreadLocal中没有存储RequestContextHolder信息
}
sendRegistrationEmail(user); // 无法获取请求头信息
});
return Result.ok();
}为什么异步线程中上下文会丢失
核心原因:ThreadLocal的线程隔离特性
ThreadLocal的设计初衷就是为了实现线程隔离(Thread Isolation):
主线程(Web请求线程)
│
├─ ThreadLocal<RequestContextHolder>
│ └─ 存储数据 {X-User-ID: 123}
│
├─ executor.execute(异步任务)
│ └─ 创建新线程(线程池中的线程)
│
↓
异步线程(新线程)
│
├─ ThreadLocal<RequestContextHolder>(❌ 不同的ThreadLocal实例!)
│ └─ 无数据(为空)
│
└─ 结果:RequestContextHolder.getRequestAttributes() 返回 null为什么这样设计?
- 线程安全:每个线程有自己的数据副本,避免并发问题
- 线程隔离:防止数据泄露
- 线程复用:线程池中的线程被复用时,需要重置ThreadLocal
ThreadLocal在线程池中的问题
ExecutorService executor = Executors.newFixedThreadPool(2);
// 第一个任务
executor.execute(() -> {
ThreadLocal<String> local = new ThreadLocal<>();
local.set("Task1 Data");
System.out.println(local.get()); // 输出:Task1 Data
});
// 第二个任务(可能复用同一个线程)
executor.execute(() -> {
ThreadLocal<String> local = new ThreadLocal<>();
System.out.println(local.get()); // ❌ 可能输出:Task1 Data(脏数据!)
// 因为线程被复用,上个任务的数据还在
});TTL解决方案
什么是TTL(TransmittableThreadLocal)?
TTL是阿里巴巴开源的库,用来解决ThreadLocal在线程池中的问题。
核心特性:在线程切换时自动传递和恢复ThreadLocal值
主线程
│
├─ TransmittableThreadLocal<Map<String>>
│ └─ 存储数据 {X-User-ID: 123}
│
├─ TtlExecutors.getTtlExecutor(executor)
│ └─ 返回TTL包装后的Executor
│
├─ ttlExecutor.execute(异步任务)
│ └─ TTL自动拦截,复制数据到新线程
│
↓
异步线程
│
├─ TransmittableThreadLocal<Map<String>>(✅ 同一份数据)
│ └─ {X-User-ID: 123}
│
└─ 结果:能继续使用原始请求数据TTL的工作原理(简化版)
// TTL内部伪代码
class TtlExecutor implements Executor {
private Executor delegate;
@Override
public void execute(Runnable task) {
// 1. 保存当前线程的所有TTL值
Map<TransmittableThreadLocal<?>, ?> captured = captureAllTtlValues();
// 2. 包装任务
Runnable wrappedTask = () -> {
// 3. 在新线程中恢复TTL值
restoreTtlValues(captured);
try {
task.run(); // 执行原任务
} finally {
// 4. 清理TTL值
cleanupTtlValues();
}
};
// 5. 提交包装后的任务
delegate.execute(wrappedTask);
}
}完整代码分析
第一步:创建请求头工具类
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class RequestHeaderUtil {
// 使用TTL存储请求头信息
private static final TransmittableThreadLocal<Map<String, String>> CONTEXT
= new TransmittableThreadLocal<>();
/**
* 保存当前请求的所有HTTP头到TTL
*/
public static void setHeader(RequestAttributes attributes) {
if (attributes == null) {
log.warn("RequestAttributes is null");
return;
}
Map<String, String> headerMap = new HashMap<>();
ServletRequestAttributes servletAttributes = (ServletRequestAttributes) attributes;
HttpServletRequest request = servletAttributes.getRequest();
// 遍历所有HTTP请求头
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String value = request.getHeader(name);
headerMap.put(name, value);
}
// ✅ 关键:存入TTL
CONTEXT.set(headerMap);
log.debug("Saved headers to TTL: {}", headerMap.keySet());
}
/**
* 获取保存的请求头
*/
public static Map<String, String> getHeader() {
return CONTEXT.get();
}
/**
* 清理TTL中的请求头,防止内存泄漏
*/
public static void removeHeader() {
CONTEXT.remove();
log.debug("Cleaned headers from TTL");
}
}第二步:异步处理业务逻辑
@Service
@Slf4j
public class UserService {
@Autowired
private Executor asyncExecutor;
@Autowired
private EmailService emailService;
@Autowired
private AuditService auditService;
/**
* 用户注册接口(同步)
*/
public void registerUser(User user) {
log.info("User registering: {}", user.getEmail());
// 保存用户信息(同步操作)
saveUser(user);
// 第一行代码:保存请求头到TTL
RequestHeaderUtil.setHeader(RequestContextHolder.getRequestAttributes());
// 第二行代码:获取TTL包装的Executor
Executor ttlExecutor = TtlExecutors.getTtlExecutor(asyncExecutor);
// 异步处理:发送邮件和记录审计日志
ttlExecutor.execute(() -> {
try {
log.info("Async processing started for user: {}", user.getEmail());
// 发送注册确认邮件
sendRegistrationEmail(user);
// 调用其他微服务记录审计日志
recordAuditLog(user);
log.info("Async processing completed for user: {}", user.getEmail());
} catch (Exception e) {
log.error("Async processing failed for user: {}", user.getEmail(), e);
} finally {
// ✅ 清理TTL,防止内存泄漏
RequestHeaderUtil.removeHeader();
}
});
}
/**
* 异步发送注册邮件
*/
private void sendRegistrationEmail(User user) {
// 在异步线程中,RequestContextHolder返回null
// 但可以通过RequestHeaderUtil获取保存的请求头
Map<String, String> headers = RequestHeaderUtil.getHeader();
String userId = headers != null ? headers.get("X-User-ID") : "unknown";
log.info("Sending registration email to {} (User-ID: {})", user.getEmail(), userId);
emailService.sendWelcomeEmail(user);
}
/**
* 异步记录审计日志
*/
private void recordAuditLog(User user) {
// 获取保存的请求头中的用户信息
Map<String, String> headers = RequestHeaderUtil.getHeader();
String operatorId = headers != null ? headers.get("X-Operator-ID") : "system";
String traceId = headers != null ? headers.get("X-Trace-ID") : "N/A";
log.info("Recording audit log: user={}, operator={}, traceId={}",
user.getEmail(), operatorId, traceId);
auditService.recordUserRegistration(user, operatorId, traceId);
}
private void saveUser(User user) {
// 保存用户到数据库
}
}第三步:Feign拦截器使用保存的请求头
@Slf4j
public class FeignRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
// 方法1:尝试获取当前Request上下文(同步请求中有效)
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
if (attributes != null) { // ✅ 同步请求
log.debug("Using RequestContextHolder (sync request)");
HttpServletRequest request = attributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
Optional.ofNullable(headerNames).ifPresent(headers -> {
while (headers.hasMoreElements()) {
String name = headers.nextElement();
String value = request.getHeader(name);
// 跳过content-length,让Feign自动计算
if (!name.equals("content-length")) {
requestTemplate.header(name, value);
log.debug("Set header from RequestContextHolder: {} = {}", name, value);
}
}
});
} else {
// ❌ 异步线程中RequestContextHolder返回null
// ✅ 使用保存在TTL中的请求头
log.debug("RequestContextHolder is null, using TTL (async request)");
Map<String, String> headers = RequestHeaderUtil.getHeader();
if (headers != null && !headers.isEmpty()) {
headers.forEach((name, value) -> {
if (!name.equals("content-length")) {
requestTemplate.header(name, value);
log.debug("Set header from TTL: {} = {}", name, value);
}
});
} else {
log.warn("No headers found in TTL");
}
}
}
}完整的请求链路图
对比:有无TTL的区别
❌ 不使用TTL(错误做法)
@PostMapping("/register")
public Result register(User user) {
saveUser(user);
// ❌ 错误做法:直接异步处理,不保存请求头
asyncExecutor.execute(() -> {
try {
sendEmail(user);
recordLog(user);
} catch (Exception e) {
log.error("Error", e);
}
});
return Result.ok();
}
private void recordLog(User user) {
// ❌ 问题:无法获取原始请求中的追踪ID
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
if (attributes == null) { // 🔴 异步线程中返回null
log.error("Cannot get request context");
return;
}
// 下面的代码永远无法执行
String traceId = attributes.getRequest().getHeader("X-Trace-ID");
// 调用下游微服务时无法传递追踪ID
auditService.recordLog(user, traceId); // traceId为null
}结果:
🔴 异步线程中无法获取请求头
🔴 下游微服务收不到追踪信息
🔴 无法追踪请求链路
🔴 日志上下文丢失
✅ 使用TTL(正确做法)
@PostMapping("/register")
public Result register(User user) {
saveUser(user);
// 第一行:保存请求头
RequestHeaderUtil.setHeader(RequestContextHolder.getRequestAttributes());
// 第二行:使用TTL包装的Executor
Executor ttlExecutor = TtlExecutors.getTtlExecutor(asyncExecutor);
// ✅ 正确做法:异步处理,TTL自动传递请求头
ttlExecutor.execute(() -> {
try {
sendEmail(user);
recordLog(user);
} catch (Exception e) {
log.error("Error", e);
} finally {
RequestHeaderUtil.removeHeader(); // 清理
}
});
return Result.ok();
}
private void recordLog(User user) {
// ✅ 在异步线程中仍能获取保存的请求头
Map<String, String> headers = RequestHeaderUtil.getHeader();
if (headers != null) {
String traceId = headers.get("X-Trace-ID");
// ✅ 调用下游微服务时能传递追踪ID
auditService.recordLog(user, traceId); // traceId正常
}
}结果:
✅ 异步线程中能获取请求头
✅ 下游微服务收到追踪信息
✅ 能追踪完整的请求链路
✅ 日志上下文保留
为什么不能用其他方案
| 方案 | 可行性 | 说明 |
|---|---|---|
| 直接使用RequestContextHolder | ❌ 不可行 | ThreadLocal无法跨线程传递 |
| 在异步前保存到局部变量 | ⚠️ 低效 | 需要手动传递每个参数,代码重复 |
| 使用普通Executor | ❌ 不可行 | ThreadLocal仍无法传递 |
| 使用TTL | ✅ 最佳 | 自动化、优雅、高效 |
| 将请求信息作为参数传递 | ⚠️ 代码污染 | 参数增多,代码复杂度上升 |
总结
为什么需要这两行代码?
- 第一行 RequestHeaderUtil.setHeader()
目的:将HTTP请求头从当前线程保存到TTL
原因:异步线程无法直接访问Spring的RequestContextHolder
作用:为下一步跨线程传递做准备
- 第二行 Executor ttlExecutor = TtlExecutors.getTtlExecutor(executor)
目的:获取TTL包装后的Executor
原因:普通Executor无法传递ThreadLocal数据
作用:自动化地将TTL数据复制到新线程
为什么不直接使用Spring的RequestContextHolder?
| 问题 | 原因 |
|---|---|
| ThreadLocal天生线程隔离 | 每个线程有独立的ThreadLocal存储,新线程中无数据 |
| 线程池复用问题 | 线程被复用时,旧数据可能污染新任务 |
| 无法跨越异步边界 | 异步任务运行在不同的线程,根本无法访问主线程的RequestContextHolder |
这样做的好处
✅ 自动化:TTL自动处理数据传递,无需手动操作
✅ 高效:避免重复编写传递逻辑
✅ 安全:finally中及时清理,防止内存泄漏
✅ 兼容:同时支持同步和异步场景
✅ 可追踪:能在微服务调用中保留链路信息
✅ 整洁:业务代码不需要感知上下文传递的细节
应用场景
这个模式适用于:
异步发送邮件、短信
异步生成报表、导出数据
定时任务中需要原始请求信息
线程池中的任务需要访问请求上下文
微服务调用需要保留用户认证信息和追踪ID
日志追踪需要保留TraceId
