springboot项目中MDC变量的线程间传递问题

/ 默认分类 / 0 条评论 / 55浏览

一、问题背景

在近期的开发过程中,使用了异步线程处理业务逻辑。但发现子线程中的日志无法打印 traceId 和 spanId。

二、MDC 源码分析

2.1 SLF4J MDC 源码

public class MDC {
    private static MDCAdapter mdcAdapter;
    
    static {
        try {
            mdcAdapter = StaticMDCBinder.SINGLETON.getMDCAdapter();
        } catch (NoClassDefFoundError ncde) {
            // 降级使用 BasicMDCAdapter
            mdcAdapter = new BasicMDCAdapter();
        }
    }
}

2.2 默认实现分析

  1. Logback 环境 (默认)
public class LogbackMDCAdapter implements MDCAdapter {
    private final ThreadLocal<Map<String, String>> threadLocal 
        = new ThreadLocal<Map<String, String>>();
        
    public void put(String key, String val) {
        Map<String, String> map = threadLocal.get();
        if (map == null) {
            map = new HashMap<String, String>();
            threadLocal.set(map);
        }
        map.put(key, val);
    }
}
  1. BasicMDCAdapter
public class BasicMDCAdapter implements MDCAdapter {
    private InheritableThreadLocal<Map<String, String>> inheritableThreadLocal 
        = new InheritableThreadLocal<Map<String, String>>() {
        @Override
        protected Map<String, String> childValue(Map<String, String> parentValue) {
            if (parentValue == null)
                return null;
            return new HashMap<String, String>(parentValue);
        }
    };
}

2.3 Spring Cloud Sleuth 集成

public class TraceFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, 
                        FilterChain chain) {
        // 生成或提取 traceId
        String traceId = generateTraceId();
        // 存储到 MDC
        MDC.put("traceId", traceId);
        try {
            chain.doFilter(request, response);
        } finally {
            MDC.clear();
        }
    }
}

三、线程传递问题深入分析

3.1 ThreadLocal vs InheritableThreadLocal

  1. ThreadLocal 工作原理
public class Thread {
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    // 每个线程独立的存储空间
    void createMap(ThreadLocal<?> firstKey, Object firstValue) {
        threadLocals = new ThreadLocal.ThreadLocalMap(firstKey, firstValue);
    }
}
  1. InheritableThreadLocal 继承机制
public class Thread {
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    
    // 创建线程时复制父线程的值
    private void init(ThreadGroup g, Runnable target, String name,
                     long stackSize, AccessControlContext acc,
                     boolean inheritThreadLocals) {
        if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        }
    }
}

3.2 为什么默认实现不支持继承

  1. 性能考虑
    • ThreadLocal 更轻量级
    • 避免不必要的数据复制
    • 减少内存使用
  2. 线程池问题
public class ThreadPoolExecutor {
    public void execute(Runnable command) {
        // 线程池复用线程时不会触发 Thread 的 init 方法
        // 导致 InheritableThreadLocal 的继承机制失效
        Worker w = getWorker();
        w.thread.run(command);
    }
}

四、解决方案详细分析

4.1 强制使用 BasicMDCAdapter

static {
    // 通过系统属性强制使用 BasicMDCAdapter
    System.setProperty("slf4j.mdc.delegate", 
        "org.slf4j.helpers.BasicMDCAdapter");
}

源码分析:

public class StaticMDCBinder {
    public MDCAdapter getMDCAdapter() {
        String delegate = System.getProperty("slf4j.mdc.delegate");
        if (delegate != null) {
            return (MDCAdapter) Class.forName(delegate).newInstance();
        }
        // 默认查找 logback 等实现
        return new LogbackMDCAdapter();
    }
}

4.2 Spring TaskDecorator 方案

public class MdcTaskDecorator implements TaskDecorator {
    @Override
    public Runnable decorate(Runnable runnable) {
        // 获取当前线程的 MDC 上下文
        Map<String, String> context = MDC.getCopyOfContextMap();
        
        return () -> {
            // 恢复上下文
            Map<String, String> previous = MDC.getCopyOfContextMap();
            if (context != null) {
                MDC.setContextMap(context);
            }
            try {
                runnable.run();
            } finally {
                // 清理现场
                if (previous != null) {
                    MDC.setContextMap(previous);
                } else {
                    MDC.clear();
                }
            }
        };
    }
}

Spring 线程池执行原理:

public class ThreadPoolTaskExecutor {
    protected void executeTask(Runnable task) {
        Runnable taskToUse = task;
        if (this.taskDecorator != null) {
            // 装饰器模式:包装任务
            taskToUse = this.taskDecorator.decorate(task);
        }
        this.threadPoolExecutor.execute(taskToUse);
    }
}