XXL-Job TraceId实现方案

53 阅读5分钟

XXL-Job TraceId实现方案

一、背景

project-job 微服务负责:

  1. XXL-Job定时任务:使用 @XxlJob 注解的定时任务
  2. 消息队列消费:使用 @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 消息队列消费者:手动设置

实现方式

  1. 从消息头中提取traceId(支持 X-Trace-IdEagleEye-TraceIDtraceId
  2. 如果消息头中没有,自动生成新的traceId,格式:MQ-{HandlerName}-{timestamp}-{uuid}
  3. 设置到MDC
  4. 对于异步任务(CompletableFuture),传递MDC上下文到异步线程
  5. 在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,优先使用消息头的值
  • 支持的消息头名称(优先级从高到低):
    1. X-Trace-Id
    2. EagleEye-TraceID
    3. traceId

四、日志输出效果

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-IdEagleEye-TraceID 消息头

6.4 日志查询

  • 使用traceId可以在日志系统中快速定位同一任务或消息的所有相关日志
  • 建议在日志系统中配置traceId字段索引,提高查询效率

七、验证方法

7.1 验证定时任务traceId

  1. 触发一个XXL-Job定时任务
  2. 查看日志,确认traceId格式为 JOB-{jobName}-{timestamp}-{uuid}
  3. 确认所有相关日志都包含相同的traceId

7.2 验证消息队列traceId

  1. 发送一条消息到队列
  2. 查看消费者日志,确认traceId格式为 MQ-{HandlerName}-{timestamp}-{uuid}
  3. 确认异步任务日志也包含相同的traceId

7.3 验证MDC传递

  1. 查看异步任务的日志,确认traceId与主线程一致
  2. 确认不会出现traceId串用的情况

八、后续优化建议

  1. 消息发送时传递traceId:在消息发送时,从当前MDC中获取traceId并设置到消息头
  2. 统一traceId格式:考虑统一所有场景的traceId格式
  3. 监控告警:对于traceId缺失的情况,可以考虑添加监控告警