XXL-Job TraceId实现方案
一、背景
project-job 微服务负责:
- XXL-Job定时任务:使用
@XxlJob注解的定时任务 - 消息队列消费:使用
@StreamListener注解的消息消费者
这些场景没有HTTP请求头,需要手动设置MDC来支持日志链路追踪。
二、实现方案
2.1 核心工具类:MdcTraceIdUtil
功能:
- 设置和获取traceId
- 生成新的traceId
- 获取MDC上下文副本(用于跨线程传递)
- 执行带traceId的任务(自动设置和清理)
关键方法:
// 设置traceId
MdcTraceIdUtil.setTraceId(traceId);
// 获取当前traceId
String traceId = MdcTraceIdUtil.getTraceId();
// 获取MDC副本(用于跨线程传递)
Map<String, String> context = MdcTraceIdUtil.getCopyOfContextMap();
// 在异步线程中恢复MDC
MdcTraceIdUtil.setContextMap(context);
// 清理MDC
MdcTraceIdUtil.clear();
2.2 XXL-Job定时任务:自动切面
位置:.../common/aspect/XxlJobTraceIdAspect.java
功能:
- 使用AOP自动拦截所有
@XxlJob注解的方法 - 自动生成traceId,格式:
JOB-{jobName}-{timestamp}-{uuid} - 在方法执行前设置MDC,执行后清理MDC
示例:
@XxlJob("businessJob")
public ReturnT<String> businessJob(String param) {
// 自动设置traceId,格式:JOB-businessJob-1234567890-abc12345
logger.info("businessJob 开始执行...");
// 所有日志都会包含traceId
}
2.3 消息队列消费者:手动设置
实现方式:
- 从消息头中提取traceId(支持
X-Trace-Id、EagleEye-TraceID、traceId) - 如果消息头中没有,自动生成新的traceId,格式:
MQ-{HandlerName}-{timestamp}-{uuid} - 设置到MDC
- 对于异步任务(
CompletableFuture),传递MDC上下文到异步线程 - 在finally块中清理MDC
关键代码:
@StreamListener(TaskHandleConsumer.CHANNEL)
public void handleTask(Message<String> message, ...) {
// 1. 提取或生成traceId
String traceId = extractTraceIdFromMessage(message);
if (traceId == null) {
traceId = String.format("MQ-Task-%d-%s",
System.currentTimeMillis(),
MdcTraceIdUtil.generateTraceId().substring(0, 8));
}
// 2. 设置到MDC
MdcTraceIdUtil.setTraceId(traceId);
try {
// 3. 保存MDC上下文,用于传递给异步任务
Map<String, String> mdcContext = MdcTraceIdUtil.getCopyOfContextMap();
// 4. 异步处理任务,传递MDC上下文
CompletableFuture.runAsync(() -> {
MdcTraceIdUtil.setContextMap(mdcContext);
try {
processTaskAsync(event);
} finally {
MdcTraceIdUtil.clear();
}
}, taskExecutor);
} finally {
// 5. 清理MDC
MdcTraceIdUtil.clear();
}
}
三、TraceId格式说明
3.1 定时任务TraceId
- 格式:
JOB-{jobName}-{timestamp}-{uuid} - 示例:
JOB-businessJob-1704067200000-abc12345 - 说明:包含job名称、时间戳和短UUID,便于识别和排序
3.2 消息队列TraceId
- 格式:
MQ-{HandlerName}-{timestamp}-{uuid} - 示例:
MQ-Task-1704067200000-abc12345(TaskHandler)
- 说明:包含Handler名称、时间戳和短UUID
3.3 从消息头获取
- 如果消息头中包含traceId,优先使用消息头的值
- 支持的消息头名称(优先级从高到低):
X-Trace-IdEagleEye-TraceIDtraceId
四、日志输出效果
4.1 定时任务日志
[INFO] 2024-01-01 12:00:00.000 [traceId=JOB-businessJob-1704067200000-abc12345, tid=JOB-businessJob-1704067200000-abc12345] [pool-1-thread-1] [businessJob.java:57] - businessJob 开始执行,时间范围: 2024-01-01 11:35:00 到 2024-01-01 11:55:00
4.2 消息队列消费日志
[INFO] 2024-01-01 12:00:00.000 [traceId=MQ-Task-1704067200000-abc12345, tid=MQ-Task-1704067200000-abc12345] [pool-2-thread-1] [TaskHandler.java:87] - 任务处理消费者接受到了消息, traceId=MQ-Task-1704067200000-abc12345, 消息={...}, msgId = xxx
4.3 异步任务日志
[INFO] 2024-01-01 12:00:00.000 [traceId=MQ-Task-1704067200000-abc12345, tid=MQ-Task-1704067200000-abc12345] [taskExecutor-1] [TaskHandler.java:151] - 开始异步处理任务,任务= {...}, 当前线程= taskExecutor-1, 线程池= taskExecutor
五、使用指南
5.1 新增XXL-Job定时任务
无需手动设置traceId,AOP会自动处理:
@Component
public class MyNewJob {
private static final Logger logger = LoggerFactory.getLogger(MyNewJob.class);
@XxlJob("myNewJob")
public ReturnT<String> execute(String param) {
// traceId已自动设置,直接使用logger即可
logger.info("开始执行任务,参数: {}", param);
// ... 业务逻辑
return ReturnT.SUCCESS;
}
}
5.2 新增消息队列消费者
需要手动设置traceId:
@Component
@EnableBinding({MyConsumer.class})
public class MyHandler {
private static final Logger logger = LoggerFactory.getLogger(MyHandler.class);
@StreamListener(MyConsumer.CHANNEL)
public void handleMessage(Message<String> message, ...) {
// 1. 提取或生成traceId
String traceId = extractTraceIdFromMessage(message);
if (traceId == null) {
traceId = String.format("MQ-MyHandler-%d-%s",
System.currentTimeMillis(),
MdcTraceIdUtil.generateTraceId().substring(0, 8));
}
// 2. 设置到MDC
MdcTraceIdUtil.setTraceId(traceId);
try {
// 3. 处理消息
logger.info("收到消息, traceId={}, payload={}", traceId, message.getPayload());
// ... 业务逻辑
} finally {
// 4. 清理MDC
MdcTraceIdUtil.clear();
}
}
private String extractTraceIdFromMessage(Message<String> message) {
if (message == null || message.getHeaders() == null) {
return null;
}
Object traceId = message.getHeaders().get("X-Trace-Id");
if (traceId == null) {
traceId = message.getHeaders().get("EagleEye-TraceID");
}
return traceId != null ? traceId.toString() : null;
}
}
5.3 异步任务中传递MDC
关键点:必须在异步线程中恢复MDC上下文
// 保存当前MDC上下文
Map<String, String> mdcContext = MdcTraceIdUtil.getCopyOfContextMap();
// 异步执行任务
CompletableFuture.runAsync(() -> {
// 在异步线程中恢复MDC
MdcTraceIdUtil.setContextMap(mdcContext);
try {
// 业务逻辑
logger.info("异步任务执行中..."); // 日志会包含traceId
} finally {
MdcTraceIdUtil.clear();
}
}, executor);
六、注意事项
6.1 线程池复用问题
- 必须在finally块中清理MDC,避免线程池复用导致traceId串用
- 异步任务中也要清理MDC
6.2 MDC传递
- MDC是线程本地存储,不会自动传递到子线程
- 使用
getCopyOfContextMap()和setContextMap()手动传递
6.3 消息头传递
- 如果希望消息队列的traceId能够传递,需要在发送消息时设置消息头
- 建议在消息发送时设置
X-Trace-Id或EagleEye-TraceID消息头
6.4 日志查询
- 使用traceId可以在日志系统中快速定位同一任务或消息的所有相关日志
- 建议在日志系统中配置traceId字段索引,提高查询效率
七、验证方法
7.1 验证定时任务traceId
- 触发一个XXL-Job定时任务
- 查看日志,确认traceId格式为
JOB-{jobName}-{timestamp}-{uuid} - 确认所有相关日志都包含相同的traceId
7.2 验证消息队列traceId
- 发送一条消息到队列
- 查看消费者日志,确认traceId格式为
MQ-{HandlerName}-{timestamp}-{uuid} - 确认异步任务日志也包含相同的traceId
7.3 验证MDC传递
- 查看异步任务的日志,确认traceId与主线程一致
- 确认不会出现traceId串用的情况
八、后续优化建议
- 消息发送时传递traceId:在消息发送时,从当前MDC中获取traceId并设置到消息头
- 统一traceId格式:考虑统一所有场景的traceId格式
- 监控告警:对于traceId缺失的情况,可以考虑添加监控告警