线程间传递Traceid

线程间传递Traceid

背景

作为一个程序员,在工作当中排查问题是很常见的,但在多线程的情况下,想通过日志跟踪问题,对于初学者是有点困难的。
在这里分享下如何快速定位多线程环境下的调用链路,方便调用日志的查看以及问题的定位

方案

在日志打印时增加Traceid, 方便整个调用链路的追踪

  1. 同步调用: 能根据日志打印的Traceid追踪到整条调用链路
  2. 异步调用: 如果不做其他处理异步调用的程序打印的日志会丢失Traceid,也就没法通过这个Traceid查看调用链路。

这时我们就需要对异步调用的程序进行处理,使得异步调用时日志文件也能输出Traceid,并通过Traceid查看调用链路

实现

异步调用的开启方式大致可为2种,
1、 new Thread()
2、线程池技术
在这里我们讲的是利用线程池执行异步操作,所以我们需要对线程池进行改造,使得其能传递Traceid,并在后续的程序执行打印日志时能输出Traceid
我们知道异步调用主要的方式有: Callable, Runnable
不错,到这里我们要做的就是对Callable, Runnable等方法进行封装,使得其能正确的帮我们传递Traceid
传递Traceid利用都了日志框架中的MDC工具
我们先定义一个工具类,用于生成Traceid

public class ThreadMdcUtil {


    public static String createTraceId() {
        String uuid = UUID.randomUUID().toString();
        return DigestUtils.md5Hex(uuid).substring(8, 24);
    }

    public static void setTraceIdIfAbsent() {
        if (MDC.get(CommonConstant.LOG_TRACE_ID) == null) {
            MDC.put(CommonConstant.LOG_TRACE_ID, createTraceId());
        }
    }

    public static String getTraceId() {
        return MDC.get(CommonConstant.LOG_TRACE_ID);
    }

    public static void setTraceId() {
        MDC.put(CommonConstant.LOG_TRACE_ID, createTraceId());
    }

    public static void setTraceId(String traceId) {
        MDC.put(CommonConstant.LOG_TRACE_ID, traceId);
    }

    public static void clear() {
        MDC.clear();
    }
}

有了Traceid,接下来要做的就是对线程里面的2个主要的方法进行改造,改造方案如下:

public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                return callable.call();
            } finally {
                MDC.clear();
            }
        };
    }

    public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }

对线程的2个主要的方法进行改造之后,我们要使得程序日志正确打印传递的Traceid 我们还需要进行其他的处理,
需要让程序需要用到封装之后的方法,不然之前做的都是无用功,那么我们需要如何处理呢?
上面提到要利用线程池,但是我们如何让线程池使用改造之后的2个方法呢?
在这我们要做的就是对线程池进行封装处理,重写线程池的方法,让其用到我们处理后的线程方法。

public class ThreadPoolMdcWrapper extends ThreadPoolTaskExecutor {

    public ThreadPoolMdcWrapper() {

    }

    @Override
    public void execute(Runnable task) {
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public void execute(Runnable task, long startTimeout) {
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), startTimeout);
    }

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

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

    @Override
    public ListenableFuture<?> submitListenable(Runnable task) {
        return super.submitListenable(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
        return super.submitListenable(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

}

继承ThreadPoolTaskExecutor ,重写线程执行的方法。
到这我们就做完了大部分的准备工作,还剩下最关键的就是让程序用到我们封装后的线程池。
我们可以在声明线程池的时候,直接使用我们封装好的线程池(因为继承了ThreadPoolTaskExecutor)

@Bean
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolMdcWrapper();
        //核心线程数,默认为1
        taskExecutor.setCorePoolSize(1);
        //最大线程数,默认为Integer.MAX_VALUE
        taskExecutor.setMaxPoolSize(200);
        //队列最大长度,一般需要设置值>=notifyScheduledMainExecutor.maxNum;默认为Integer.MAX_VALUE
        taskExecutor.setQueueCapacity(2000);
        //线程池维护线程所允许的空闲时间,默认为60s
        taskExecutor.setKeepAliveSeconds(60);
        //线程池对拒绝任务(无线程可用)的处理策略
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        // 初始化线程池
        taskExecutor.initialize();
        return  taskExecutor;
    }

到这我们所做的准备工作,改造工作也就结束了,剩下的就是使用了。只要在程序异步调用时,利用声明好的taskExecutor线程池进行调用,就可以在线程上下文正确传递Traceid了。

以上为个人总结,有不好的地方或错误的地方,欢迎各位大佬指正
也欢迎各位大佬给出更好的,更简便的方案

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值