使用TransmittableThreadLocal实现异步场景日志链路追踪

本文介绍了在Java环境中,如何通过TransmittableThreadLocal解决线程间MDC(Mapped Diagnostic Context)日志链路追踪的问题。文章详细阐述了MDC在多线程和线程池场景下的不足,并提出使用自定义的TtlMDCAdapter结合TransmittableThreadLocal来改进,同时展示了如何在Spring应用中启用这个解决方案,以及如何增强线程池以保持日志追踪的准确性。
  • 背景
  • 解决方案

背景

    在生产环境排查问题往往都是通过日志,但对于巨大的日志量,如何针对某一个操作进行一整个日志链路的追踪就显得尤为重要,在Java语言第三方的日志工具都提了日志链路追踪的方案,比如logback的MDC,MDC的使用也很简单,就是在业务的开始put一个key-value,这个key-value就能贯穿整个线程的执行流程,使用代码如下:

MDC.put("traceId", UUID.randomUUID().toString());

    MDC虽然提供了一个现成的整个执行流程的日志追踪的方案,但是也只是一个线程,假如一个线程中又启动了另一个线程呢,这时MDC就无法完成完整的链路追踪工作了,因为MDC是基于ThreadLocal实现的,所以当一个线程中启动另一个线程的时候两个线程的TraceId就隔离开了,也就无法做到日志链路追踪。

解决方案

线程间参数传递技术选型

  • InheritableThreadLocal

    InheritableThreadLocal是JDK实现的一种线程传递解决方案,由当前线程创建的线程,将会继承当前线程里ThreadLocal保存的值,但由于InheritableThreadLocal是在创建线程是解决ThreadLocal的传值问题,但是线程不可能一直创建,在工程代码中往往都是使用线程池,但是,递交异步任务使相应的ThreadLocal的值就无法传递过去了。

  • TransmittableThreadLocal真正的解决方案

    TransmittableThreadLocal是阿里巴巴开源的,用于解决在使用线程池等会缓存线程的组件情况下传递ThreadLocal问题的InheritableThreadLocal扩展,具体的实现以后有机会深入研究,该解决方案常用于以下几个场景:

分布式跟踪系统
应用容器或上层框架跨应用代码给下层SDK传递信息
日志收集记录系统上下文

重写MDCAdapter

    由于MDC是基于ThreadLocal实现的,所以我们现在需要做的就是重写MDCAdapter,使系统再使用MDC时实际上是使用的我们自己实现的MDCAdapter,自定义的MDCAdapter时要注意包名应该与logback的MDCAdapter一致,因为我们要在程序启动的时候替换MDC中的MDCAdapter,MDC的MDCAdapter是包级私有,所以自定义MDCAdapter的包名一定要哥logback的MDCAdapter一致,自定义MDCAdapter代码如下:

package org.slf4j;


import com.alibaba.ttl.TransmittableThreadLocal;
import org.slf4j.spi.MDCAdapter;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * 重写logback的LogbackMDCAdapter,注意MDC是MDCAdapter是包级私有,所以重写的报名应该是org.slf4j
 * 用TransmittableThreadLocal替换ThreadLocal,解决多线程,异步情况x-glabal-sessionId无法传递问题
 * @author liupenghui
 * @date 2021/6/30 2:38 下午
 */
public class TtlMDCAdapter implements MDCAdapter {
   
   

    private final ThreadLocal<Map<String, String>> copyOnInheritThreadLocal = new TransmittableThreadLocal<>();

    private static final int WRITE_OPERATION = 1;
    private static final int MAP_COPY_OPERATION = 2;

    static TtlMDCAdapter mtcMDCAdapter;

    static {
   
   
        mtcMDCAdapter = new TtlMDCAdapter();
        // 替换MDC的MDCAdapter
        MDC.mdcAdapter = mtcMDCAdapter;
    }

    public static MDCAdapter getInstance() {
   
   
        return mtcMDCAdapter;
    }

    final ThreadLocal<Integer> lastOperation = new ThreadLocal<Integer>();

    private Integer getAndSetLastOperation(int op) {
   
   
        Integer lastOp = lastOperation.get();
        lastOperation.set(op);
        return lastOp;
    }

    private boolean wasLastOpReadOrNull(Integer lastOp) {
   
   
        return lastOp == null || lastOp.intValue() == MAP_COPY_OPERATION;
    }

    private Map<String, String> duplicateAndInsertNewMap(Map<String, String> oldMap) {
   
   
        Map<String, String> newMap = Collections.synchronizedMap(new HashMap<String, String>());
        if (oldMap != null) {
   
   
            // we don't want the parent thread modifying oldMap while we are
            // iterating over it
            synchronized (oldMap) {
   
   
                newMap.putAll(oldMap);
            }
        }

        copyOnInheritThreadLocal.set(newMap);
        return newMap;
    }

    /**
     * Put a context value (the <code>val</code> parameter) as identified with the
     * <code>key</code> parameter into the current thread's context map. Note that
     * contrary to log4j, the <code>val</code> parameter can be null.
     * <p/>
     * <p/>
     * If the current thread does not have a context map it is created as a side
     * effect of this call.
     *
     * @throws IllegalArgumentException in case the "key" parameter is null
     */
    @Override
    public void put(String key, String val) throws IllegalArgumentException {
   
   
        if (key == null) {
   
   
            throw new IllegalArgumentException("key cannot be null"
评论 11
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Redick01

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值