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,这正是分布式追踪系统设计精妙之处:通过线程上下文在同步处理链路上自动传递追踪信息。

相关推荐
小鸡脚来咯8 小时前
java web后端开发流程
java·开发语言·git
北友舰长8 小时前
基于Springboot+thymeleaf快递管理系统的设计与实现【Java毕业设计·安装调试·代码讲解】
java·spring boot·mysql·校园管理·快递·快递系统
我爱烤冷面8 小时前
kotlin项目实现Java doc的方案:使用Dokka
java·开发语言·kotlin·dokka
jian110588 小时前
android java转kotlin,kotlin转java
android·java·kotlin
长征coder8 小时前
SpringCloud服务优雅下线LoadBalancer 缓存配置方案
java·后端·spring
历程里程碑8 小时前
C++ 4:内存管理
java·c语言·开发语言·数据结构·c++·笔记·算法
没有bug.的程序员8 小时前
微服务的本质:不是拆服务,而是拆复杂度
java·jvm·spring·微服务·云原生·容器·架构
武子康8 小时前
Java-200 RabbitMQ 架构与 Exchange 路由:fanout/direct/topic/headers
java·架构·消息队列·系统架构·rabbitmq·java-rabbitmq·mq
计算机学姐8 小时前
基于SSM的社区外来务工人员管理系统【2026最新】
java·vue.js·java-ee·tomcat·maven·intellij-idea·mybatis