前言

在生产环境中,由于处在并发环境,所以日志输出的顺序散落在各个不同行,通过traceId就能够快速定位到同一个请求的多个不同的日志输出,可以很方便地跟踪请求并定位问题。但是,如果在代码中使用了多线程,那么就会发现,新开的线程不会携带父线程traceId。于是,通过继承父线程的MDC上下文信息,使得新开的线程与父线程保持一致的traceId

MDC说明:

MDC(Mapped Diagnostic Context)是一种常用的日志记录技术,MDC可以将关键信息存储在线程上下文中,并在需要时将其传递到调用链的不同组件中。

使用MDC传递日志的好处:

  1. 方便跟踪请求:通过 MDC,可以在整个请求生命周期中记录和传递关键信息,例如请求 ID、用户 ID 等,这样可以方便地跟踪请求并定位问题。
  2. 提高调试效率:MDC 可以存储调用链中各个组件的上下文信息,从而使得在调试时可以更快速地诊断问题,缩短故障排除时间。
  3. 支持分布式系统:在分布式系统中,MDC 可以在不同节点之间传递关键信息,使得在跨节点调用时可以快速定位问题。
  4. 提高代码可读性:MDC 记录的上下文信息可以被日志输出格式化为易于阅读的形式,提升代码可读性。

实现代码:

/**
 * 继承ThreadPoolTaskExecutor,实现多线程处理任务时传递日志traceId
 */
public class ThreadPoolTaskExecutorMdcUtil extends ThreadPoolTaskExecutor {

    @Override
    public void execute(Runnable task) {
        super.execute(wrap(task));
    }

    @Override
    public <T> Future<T> submit(Callable<T> task) {
        return super.submit(wrap(task));
    }

    @Override
    public Future<?> submit(Runnable task) {
        return super.submit(wrap(task));
    }

    private <T> Callable<T> wrap(final Callable<T> callable) {
        // 获取当前线程的MDC上下文信息
        Map<String, String> context = MDC.getCopyOfContextMap();
        return () -> {
            if (context != null) {
                // 传递给子线程
                MDC.setContextMap(context);
            }
            try {
                return callable.call();
            } finally {
                // 清除MDC上下文信息,避免造成内存泄漏
                MDC.clear();
            }
        };
    }

    private Runnable wrap(final Runnable runnable) {
        Map<String, String> context = MDC.getCopyOfContextMap();
        return () -> {
            if (context != null) {
                MDC.setContextMap(context);
            }
            try {
                runnable.run();
            } finally {
                // 清除MDC上下文信息,避免造成内存泄漏
                MDC.clear();
            }
        };
    }
}

之后只要像正常的使用线程池一样使用ThreadPoolTaskExecutorMdcUtil类即可。

例如,注入一个线程池Bean代码示例:

@Bean("thread-pool-receive")
public ThreadPoolTaskExecutor receiveThreadPoolExecutor() {
    // new的是自定义的线程池
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutorMdcUtil();
    executor.setCorePoolSize(1);
    executor.setMaxPoolSize(10);
    // 缓存队列
    executor.setQueueCapacity(10000);
    // 允许线程的空闲时间60秒:
    executor.setKeepAliveSeconds(60);
    // 线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
    executor.setThreadNamePrefix("test-");
    // 拒绝策略为调用者执行
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    executor.initialize();
    return executor;
}