traceId 传递-MQ

mq 发送消息

java 复制代码
    private <T extends BaseEvent> MessageBuilder<T> toMessageBuilder(final T event) {
        if (StringUtils.isBlank(event.keys())) {
            throw new RuntimeException("keys是必填项");
        }
        // 获取tag,默认使用类名
        String tags = StringUtils.defaultString(event.tags(), event.getClass().getSimpleName());
        // 构建消息
        MessageBuilder<T> messageBuilder = MessageBuilder.withPayload(event)
            .setHeader(RocketMQHeaders.TAGS, tags)
            .setHeader(RocketMQHeaders.KEYS, event.keys());
        String traceId = MDC.get(Constants.MDC_TRACE_ID);
        if (StringUtils.isNotBlank(traceId)) {
            messageBuilder.setHeader(RocketMQConsts.Header.TRACE_ID, traceId);
        }
        String env = RequestThread.getValue(Constants.ENV);
        if (StringUtils.isNotBlank(env)) {
            messageBuilder.setHeader(RocketMQConsts.Header.ENV, env);
        }
        String desc = event.desc();
        if (StringUtils.isNotBlank(desc)) {
            messageBuilder.setHeader(RocketMQConsts.Header.DESC, desc);
        }
        String producerApplicationName = environment.getProperty(Constants.SPRING_APPLICATION_NAME, DEFAULT_PRODUCER);
        messageBuilder.setHeader(RocketMQConsts.Header.PRODUCER, producerApplicationName);
        return messageBuilder;
    }

mq消费

java 复制代码
  public <T extends BaseEvent> void process(final String key, final Message<T> message, final Consumer<T> function) {
        String cacheKey = RedisKeyUtil.generate(REDIS_REPEAT_PREFIX_KEY, key);
        KeyInfo keyInfo = KeyInfo.builder()
            .prefix(REDIS_REPEAT_PREFIX_KEY)
            .keys(new String[]{key})
            .waitTime(3)
            .timeUnit(TimeUnit.SECONDS)
            .build();
        // 获取消息体
        T event = message.getPayload();
        try {
            // 获取链路追踪id
            String traceId = getHeaderValue(message, RocketMQConsts.Header.TRACE_ID);
            MDC.put(Constants.MDC_TRACE_ID, StringUtils.isBlank(traceId) ? key : traceId);
            // 设置调度tag环境
            String env = getHeaderValue(message, RocketMQConsts.Header.ENV);
            RequestThread.addParam(Constants.ENV, env);
            //对相同key的消息加锁,保证相同的消息是串行消费
            lockService.lock(keyInfo, () -> {
                //判断该消息是否已处理
                if (redisCache.exists(cacheKey)) {
                    log.info("[MQ消息-已处理]--[{}]:{}", key, ReflectUtils.processObject(event));
                    return null;
                }
                log.info("[MQ消息-开始处理]--[{}]:{}", key, ReflectUtils.processObject(event));
                function.accept(event);
                log.info("[MQ消息-处理完成]--{}", key);
                //缓存处理结果一小时,一个小时内不重复消费
                redisCache.setCacheObject(cacheKey, DateUtil.now(), 60, TimeUnit.MINUTES);
                return null;
            });
        } catch (LockException e) {
            //加锁失败,说明有线程正在消费
            log.warn("[MQ消息-正在处理]--[{}]:{}", key, ReflectUtils.processObject(event));
            throw e;
        } catch (Exception e) {
            log.error("[MQ消息-处理异常]--[{}]:{}", key, event, e);
            throw e;
        } finally {
            MDC.clear();
            RequestThread.clear();
        }
    }

问题

为什么mq消费可以从mac中获取traceId 传递给消息请求头

这个代码展示了 TraceId 如何从 HTTP 请求上下文传播到 MQ 消息 的完整链路。让我详细解释为什么这里可以从 MDC 获取到 traceId:

🔄 完整的传播链路

🎯 为什么可以从 MDC 获取 traceId?

关键:在同一个线程内!

java

复制代码
// 时间线分析(假设单线程同步处理):
1. HTTP请求进入 → HeaderFilter.doFilterInternal()
   ↓
2. HeaderFilter设置MDC:
   String traceId = request.getHeader("traceId");
   MDC.put("traceId", traceId);  // 存入当前线程的ThreadLocal
   ↓
3. 调用Controller → Service → MQ发送方法
   ↓
4. MQ发送方法执行:
   String traceId = MDC.get("traceId");  // 从同一个线程的ThreadLocal获取!
   ↓
5. HeaderFilter清理:
   finally { MDC.clear(); }  // 清理,但已经用过了

线程状态可视化

java

复制代码
public class ThreadStateDemo {
    
    public void processOrder() {
        // 时间点1:HeaderFilter执行前
        // 线程:http-nio-8080-exec-1
        // MDC状态:{}
        
        // HeaderFilter执行
        MDC.put("traceId", "TRACE-123");
        // MDC状态:{"traceId": "TRACE-123"}
        
        // 时间点2:Controller执行
        // 仍然是同一个线程:http-nio-8080-exec-1
        String traceId1 = MDC.get("traceId");  // "TRACE-123"
        
        // 时间点3:Service执行,调用MQ发送
        // 仍然是同一个线程:http-nio-8080-exec-1
        sendMQEvent();
        
        // sendMQEvent内部:
        String traceId2 = MDC.get("traceId");  // "TRACE-123" ✅
        
        // 时间点4:HeaderFilter finally清理
        MDC.clear();
        // MDC状态:{}
    }
    
    private void sendMQEvent() {
        // 关键:这里和调用者在同一个线程!
        System.out.println("当前线程: " + Thread.currentThread().getName());
        // 输出:http-nio-8080-exec-1
        
        String traceId = MDC.get("traceId");
        System.out.println("获取traceId: " + traceId);  // "TRACE-123"
    }
}

📊 完整的上下文传播表

阶段 组件 上下文存储位置 如何获取 是否同一线程
网关接收 TraceGlobalFilter 请求Header request.getHeader() N/A
网关传递 TraceGlobalFilter 请求Header header.set() 网关线程
业务服务接收 HeaderFilter 请求Header request.getHeader() 业务线程
业务服务存储 HeaderFilter MDC (ThreadLocal) MDC.put() 业务线程
Controller使用 Controller方法 MDC MDC.get() 同一业务线程
Service使用 Service方法 MDC MDC.get() 同一业务线程
MQ发送 MQ发送器 MDC MDC.get() 同一业务线程
MQ消息头 RocketMQ消息 Message Header setHeader() 同一业务线程

🔧 代码执行流程详解

1. 请求进入(设置MDC)

java

复制代码
@Order(Ordered.HIGHEST_PRECEDENCE)
public class HeaderFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, ...) {
        try {
            // 从HTTP header获取traceId
            String traceId = request.getHeader(Constants.MDC_TRACE_ID);
            
            // 设置到当前线程的MDC
            MDC.put(Constants.MDC_TRACE_ID, 
                StringUtils.defaultString(traceId, 
                    String.valueOf(System.currentTimeMillis())));
            
            // 继续处理请求
            chain.doFilter(request, response);  // → 进入Controller
            
        } finally {
            MDC.clear();  // 请求处理完后清理
        }
    }
}

2. 业务处理(使用MDC)

java

复制代码
@Service
public class OrderService {
    
    @Autowired
    private EventPublisher eventPublisher;
    
    public void createOrder(OrderRequest request) {
        // 这里可以直接使用MDC中的traceId
        String currentTraceId = MDC.get(Constants.MDC_TRACE_ID);
        log.info("创建订单,traceId: {}", currentTraceId);
        
        // 执行业务逻辑...
        Order order = saveOrder(request);
        
        // 发送MQ事件
        OrderCreatedEvent event = new OrderCreatedEvent(order);
        eventPublisher.publish(event);  // → 进入MQ发送代码
    }
}

3. MQ发送(从MDC获取)

java

复制代码
public class EventPublisher {
    
    private <T extends BaseEvent> MessageBuilder<T> toMessageBuilder(final T event) {
        // 关键:这里和Service方法在同一个线程
        // 所以可以获取到MDC中的traceId
        
        String traceId = MDC.get(Constants.MDC_TRACE_ID);  // ✅ 能获取到!
        if (StringUtils.isNotBlank(traceId)) {
            // 设置到MQ消息头
            messageBuilder.setHeader(RocketMQConsts.Header.TRACE_ID, traceId);
        }
        
        // 同样获取其他上下文
        String env = RequestThread.getValue(Constants.ENV);  // ThreadLocal
        if (StringUtils.isNotBlank(env)) {
            messageBuilder.setHeader(RocketMQConsts.Header.ENV, env);
        }
        
        return messageBuilder;
    }
}

📝 总结:为什么能从MDC获取traceId

核心原因:MQ发送代码与HTTP请求处理在同一个Tomcat线程内执行。

线程连续性:HTTP请求线程 → Controller → Service → MQ发送,都在同一个线程

MDC的线程绑定:MDC基于ThreadLocal,线程内共享

生命周期匹配:在HeaderFilter清理MDC之前,MQ发送已经完成

设计意图:这正是MDC的设计目的------在请求处理链路中共享上下文

关键时间线:

所以你的MQ发送代码能正确获取traceId,这正是分布式追踪系统设计精妙之处:通过线程上下文在同步处理链路上自动传递追踪信息。

相关推荐
Javatutouhouduan3 分钟前
大厂Java岗最新面试真题汇总!
java·java面试·后端开发·java编程·java程序员·互联网大厂·java八股文
逻辑驱动的ken4 分钟前
Java高频面试考点场景题23
java·开发语言·数据库·面试·职场和发展·哈希算法
xxjj998a11 分钟前
PHP vs Java:核心区别与应用场景全解析
java·开发语言·php
用户2986985301426 分钟前
Java 从零生成 Word 文档:段落、图片与表格操作
java·后端
2401_8332693028 分钟前
Java IO流:从字节到字符的桥梁
java·开发语言
月落归舟42 分钟前
深入剖析乐观锁背后的原理
java·乐观锁
SimonKing1 小时前
OpenCode 在 IDEA 中使用 ACP 协议 VS 直接使用 TUI,哪个编程方式更是你的菜?
java·后端·程序员
NE_STOP1 小时前
Redis--持久化之AOF
java
budingxiaomoli1 小时前
注册中心的其他实现-Nacos
java·spring cloud·微服务
大大大大晴天️1 小时前
Flink技术实践-Flink重启策略选型指南
java·大数据·flink