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

相关推荐
驭渊的小故事3 小时前
MyBatis XML 开发实战:从配置到 CRUD 全解析
xml·java·mybatis
哦虎!7 小时前
【JavaEE】网络初识
java
for_ever_love__8 小时前
MySQL ORDER BY 排序优化:从 Using filesort 到索引有序扫描(含双路 / 单路排序原理)
java·mysql·优化·存储·by·order·filesort
我是人✓9 小时前
Java文件上传三方式对比:从传统到SpringBoot
java·开发语言
欣栀♡9 小时前
JVM 频繁 Full GC 先看日志括号里的 cause,jstat 的 FGC 在 JDK 8 上会骗人
java·jvm
IT研究室9 小时前
最新计算机毕业设计选题推荐-基于spring boot的葡萄酒文化宣传网站-网站-文档指导-Java-springboot
java·spring boot·课程设计
慧都小项9 小时前
MyEclipse 中创建和测试 JAX-RS REST 接口:从项目配置到 REST Explorer
java·restful·myeclipse·jax-rs
Wang's Blog9 小时前
Java 项目实战: 交友项目 - 入门案例:编写 Dubbo 服务提供者
java·dubbo·交友
秋饼10 小时前
Spring AI 2.0 结构化输出企业级实战:从 BeanOutputConverter 源码到生产级解析容错
java·ai·技术分享·后端开发
摇滚侠10 小时前
《计算机是如何工作的:人人都能懂的计算机软硬件工作原理》第 9 章 机器码与汇编语言 阅读笔记 9
java·开发语言·笔记