文章目录
一、MDC是什么
MDC全称为Mapped Diagnostic Context,是著名日志框架SLF4J提供的一种方便多线程条件下记录日志的工具类,在分布式链路跟踪系统实现上,MDC绝对算是是一把核心利器。MDC的直观理解,可看做一个与当前线程绑定的Map,这个Map用来存储线程私有信息。怎么做到与线程绑定呢,用到的就是JDK的ThreadLocal,楼主之前也有专门问章分析过ThreadLocal,见【了不起的ThreadLocal】一、源码分析
1.1 MDC常用API
使用MDC非常简单,定义的常用API也就如下6个静态方法
- void put(String key, String val)
- String get(String key)
- void remove(String key)
- void clear()
- Map<String, String> getCopyOfContextMap()
- void setContextMap(Map<String, String> contextMap)
1.2 MDC数据结构
MDC数据结构如下图所示,简单概括下:
- MDC持有一个MDCAdapter,这个MDCAdapter是一个接口,实现交给具体日志组件,比如在Logback的实现类就是LogbackMDCAdapter;
- MDC可视作一个门面类,对外交互的所有API都会经过MDC,再委托给具体的MDCAdapter来执行。
二、MDC的SPI机制
在Java生态里面,日志组件非常丰富,比较普及的有Log4j、Logback、Log4j2等;前面讲到MDC持有的MDCAdapter仅仅是一个接口,运行时具体用到的是哪个MDCAdapter的实现,完全交给应用系统来决定。比如应用系统采用的是Logback,则MDC绑定的将是LogbackMDCAdapter。这个绑定过程是通过SLF4J定义的一套SPI扩展机制来实现的,下文会结合MDC部分源码来更好地理解下这个SPI机制。
2.1 LogbackMDCAdapter绑定过程
如上图所示,MDC完整类路径为org.slf4j.MDC
,注意看上图slf4j-api这个jar包,是没有org.slf4j.impl
这个包的。这里就得提到第一个规范:SLF4J的SPI机制,强制要求实现SLF4J日志标准的组件,必须将spi实现类的绑定逻辑统一放在org.slf4j.impl
这个包下面,并且类名和部分方法名是固定,不能瞎写。
不信?对照Logback这个jar包结构看下。
以Logback为例,来看看具体的绑定过程:
- 1.当客户端调用MDC的任一静态方法时,将触发MDC这个类的初始化,进而触发static静态代码块执行(这个机制是JVM的规范,static代码块只会被执行一次);
- 2.在static静态代码块中,主要有两条途径加载具体MDC绑定类: 第一种是通过调用
org.slf4j.impl
这个包的StaticMDCBinder.getSingleton().getMDCA()
,很遗憾logback里面没有这个方法,故抛出异常;第二种是调用org.slf4j.impl
包StaticMDCBinder.SINGLETON.getMDCA()
方法,刚好Logback就是这么定义的,找到了!返回的就是LogbackMDCAdapter,故最终MDC成功绑定的就是LogbackMDCAdapter;
到这来,SPI就说完了!没错,这个SPI机制就是这么简单粗暴得不敢相信!
注意:下面的代码在slf4j里面
注意: org.slf4j.impl.StaticMDCBinder这个对象引用,实际SLF4J 这个jar包里面是没有这个类的;但MDC.java文件又引入了,为何编译器不报错?原因在于用到了animal-sniffer-maven-plugin这个Maven插件,这个插件的细节楼主还没仔细看,感兴趣的同学可以自行研究下!
import java.io.Closeable;
import java.util.Map;
import org.slf4j.helpers.NOPMDCAdapter;
import org.slf4j.helpers.BasicMDCAdapter;
import org.slf4j.helpers.Util;
// 注意这个包的引用(实际SLF4J里面没有这个类)
import org.slf4j.impl.StaticMDCBinder;
import org.slf4j.spi.MDCAdapter;
public class MDC {
static final String NULL_MDCA_URL = "http://www.slf4j.org/codes.html#null_MDCA";
static final String NO_STATIC_MDC_BINDER_URL = "http://www.slf4j.org/codes.html#no_static_mdc_binder";
static MDCAdapter mdcAdapter;
/**
* As of SLF4J version 1.7.14, StaticMDCBinder classes shipping in various bindings
* come with a getSingleton() method. Previously only a public field called SINGLETON
* was available.
*
* @return MDCAdapter
* @throws NoClassDefFoundError in case no binding is available
* @since 1.7.14
*/
private static MDCAdapter bwCompatibleGetMDCAdapterFromBinder() throws NoClassDefFoundError {
try {
// logback没有这个方法,故会进入到catch代码块里面
return StaticMDCBinder.getSingleton().getMDCA();
} catch (NoSuchMethodError nsme) {
// logback里面能找到
// binding is probably a version of SLF4J older than 1.7.14
return StaticMDCBinder.SINGLETON.getMDCA();
}
}
// MDC类初始化时会执行
static {
try {
// 绑定具体的MDCAdapter实现
mdcAdapter = bwCompatibleGetMDCAdapterFromBinder();
} catch (NoClassDefFoundError ncde) {
mdcAdapter = new NOPMDCAdapter();
String msg = ncde.getMessage();
if (msg != null && msg.contains("StaticMDCBinder")) {
Util.report("Failed to load class \"org.slf4j.impl.StaticMDCBinder\".");
Util.report("Defaulting to no-operation MDCAdapter implementation.");
Util.report("See " + NO_STATIC_MDC_BINDER_URL + " for further details.");
} else {
throw ncde;
}
} catch (Exception e) {
// we should never get here
Util.report("MDC binding unsuccessful.", e);
}
}
}
注意:这里的代码在Logback里面,并非slf4j
public class StaticMDCBinder {
/**
* The unique instance of this class.
*/
public static final StaticMDCBinder SINGLETON = new StaticMDCBinder();
private StaticMDCBinder() {
}
/**
* Currently this method always returns an instance of
* {@link StaticMDCBinder}.
*/
public MDCAdapter getMDCA() {
return new LogbackMDCAdapter();
}
public String getMDCAdapterClassStr() {
return LogbackMDCAdapter.class.getName();
}
}
三、MDC源码解析
3.1 MDC源码
知道MDC干活全靠委派给MDCAdapter,那MDC的源码其实就不用细看了,以下瞟一眼,大概知道有哪几个API,并且意识到get、put、remove方法都要求key不能null,就可以略过了。
public class MDC {
static final String NULL_MDCA_URL = "http://www.slf4j.org/codes.html#null_MDCA";
static final String NO_STATIC_MDC_BINDER_URL = "http://www.slf4j.org/codes.html#no_static_mdc_binder";
static MDCAdapter mdcAdapter;
public static void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key parameter cannot be null");
}
if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
}
mdcAdapter.put(key, val);
}
public static String get(String key) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key parameter cannot be null");
}
if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
}
return mdcAdapter.get(key);
}
public static void remove(String key) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key parameter cannot be null");
}
if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
}
mdcAdapter.remove(key);
}
public static void clear() {
if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
}
mdcAdapter.clear();
}
/**
* Return a copy of the current thread's context map, with keys and values of
* type String. Returned value may be null.
*
* @return A copy of the current thread's context map. May be null.
* @since 1.5.1
*/
public static Map<String, String> getCopyOfContextMap() {
if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
}
return mdcAdapter.getCopyOfContextMap();
}
/**
* Set the current thread's context map by first clearing any existing map and
* then copying the map passed as parameter. The context map passed as
* parameter must only contain keys and values of type String.
*
* @param contextMap
* must contain only keys and values of type String
* @since 1.5.1
*/
public static void setContextMap(Map<String, String> contextMap) {
if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
}
mdcAdapter.setContextMap(contextMap);
}
}
3.2 LogbackMDCAdapter源码
LogbackMDCAdapter是真正的主角,MDC的数据读/写都交给了它。太阳底下无新鲜事,LogbackMDCAdapter最核心的数据存储结构就是2个ThreadLocal,围绕这2个ThreadLocal完成MDC的全部接口功能。故看懂LogbackMDCAdapter的关键在于看懂这2个ThreadLocal的设计用意!
public class LogbackMDCAdapter implements MDCAdapter {
// MDC真正用来存数据的容器
final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal<Map<String, String>>();
private static final int WRITE_OPERATION = 1;
private static final int MAP_COPY_OPERATION = 2;
// 记录上一次操作类型:写为1(put、remove、cleare都是写),copy map为2
// keeps track of the last operation performed
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;
}
}
了解源码最好的办法,就是把代码run起来。楼主写了几个测试类,下面就按照测试类的执行思路展开源码分析。
3.2.1 Write -> Write 场景分析
- 测试Case1
public class MdcTest {
@Test
public void testMdc() {
// 打印出当前绑定的MDCAdapter
System.out.println(MDC.getMDCAdapter().getClass().getName());
// 第一次操作put
MDC.put("a", "123");
dumpMdc();
// 第二次操作put
MDC.put("b", "456");
dumpMdc();
// 第三次操作put
MDC.remove("b");
dumpMdc();
}
public static void dumpMdc() {
System.out.println(MDC.getCopyOfContextMap());
}
}
- 输出
ch.qos.logback.classic.util.LogbackMDCAdapter
{a=123}
{a=123, b=456}
{a=123}
为便于直观理解,配几幅图来动态分析代码执行流程(靠想象力自行在脑海模拟JVM运行上述代码)
-
LogbackMDCAdapter 类初始状态
简单描述下:LogbackMDCAdapter这个类初始化之后,copyOnThreadLocal、lastOperation两个final变量(ps: final变量实际存放在方法区,图上画在栈内存纯属偷懒),分别指向`ThreadLocal<Map<String, String>> copyOnThreadLocal 和 ThreadLocal lastOperation 这两个ThreadLocal;此时,当前线程(main线程)的ThreadLocalMap(即threadLocals属性)还没存放值。
-
a.第一次操作执行
put("a", "123")
public class LogbackMDCAdapter implements MDCAdapter {
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;
}
// 第一次put操作时,oldMap为null
private Map<String, String> duplicateAndInsertNewMap(Map<String, String> oldMap) {
// new了一个新map对象
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);
}
}
// 对ThreadLocal的某个Entry赋值,假设就是Entry[0], 其key就是copyOnThreadLocal,其value为空map
copyOnThreadLocal.set(newMap);
return newMap;
}
public void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key cannot be null");
}
// 第一次put操作时,因为main线程的ThreadLocalMap还是空的,故从copyOnThreadLocal定位不到任何Entry,故返回的oldMap为null
Map<String, String> oldMap = copyOnThreadLocal.get();
// 记录当前操作为写(new一个Entry[3],并将value设置为1),返回的lastOp为null
Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);
if (wasLastOpReadOrNull(lastOp) || oldMap == null) {
// 进到这个分支,并且oldMap为null;
Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);
// 调用duplicateAndInsertNewMap之后的结果是创建出了Entry[0], 并且其value为一个空map
// 将a=123塞到newMap, 由于newMap和Entry[0].value指向的是同一个map对象,故最终效果就是a=123写到了main线程的ThreadLocal里面了(理解这个语句非常重要!!)
newMap.put(key, val);
} else {
oldMap.put(key, val);
}
}
}
这里留一个问题,为什么lastOp为copy map时要new一个map出来?
- b.第二次操作执行
put("b", "456")
public class LogbackMDCAdapter implements MDCAdapter {
public void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key cannot be null");
}
// 第二次put操作时,从copyOnThreadLocal定位到Entry[0]的value为{a=123}
Map<String, String> oldMap = copyOnThreadLocal.get();
// 从lastOperation定位到Entry[3]的value为1,故返回的lastOp为1;并记录当前操作为写(将Entry[3]的value从1设置为1)
Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);
if (wasLastOpReadOrNull(lastOp) || oldMap == null) {
Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);
newMap.put(key, val);
} else {
// lastOp为1且oldMap又不为null,故进到这个分支
// 直接把当前写操作内容放入oldMap,最终Entry[0].value为{a=123, b=456}
oldMap.put(key, val);
}
}
}
- c.第三次操作
remove("b")
public class LogbackMDCAdapter implements MDCAdapter {
public void remove(String key) {
if (key == null) {
return;
}
// 从copyOnThreadLocal定位到Entry[0]的value为{a=123, b=456}
Map<String, String> oldMap = copyOnThreadLocal.get();
if (oldMap == null)
return;
// 从lastOperation定位到Entry[3]的value为1,故返回的lastOp=1
Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);
if (wasLastOpReadOrNull(lastOp)) {
Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);
newMap.remove(key);
} else {
// lastOp=1,进到这个分支;直接从oldMap做删除操作,删除b之后oldMap为{a=123}
oldMap.remove(key);
}
}
}
3.2.2 Write -> Copy map -> Write 场景分析
在这一小节,回答前面提出的「为什么lastOp为copy map的时候要new一个map出来」,还是结合一个测试Case来分析。
- 测试Case2
public class MdcTest {
@Test
public void testLogbackMDCAdapter() {
LogbackMDCAdapter mdcAdapter = new LogbackMDCAdapter();
// 第一次操作put
mdcAdapter.put("a", "123");
// 注意: 方法名虽然看着像在做copy map,实际没有记录成copy map操作
System.out.println(mdcAdapter.getCopyOfContextMap());
// 第二次操作 copy map(这个方法里面里面才真正记录了 copy map操作)
Map<String, String> snapshot = mdcAdapter.getPropertyMap();
System.out.println("snapshot: " + snapshot);
// 第三次操作put
mdcAdapter.put("b", "456");
System.out.println(mdcAdapter.getCopyOfContextMap());
System.out.println("snapshot: " + snapshot);
}
}
- 输出
{a=123}
snapshot: {a=123}
{a=123, b=456}
snapshot: {a=123}
-
a.第一次操作执行
put("a", "123")
第一次put操作后,同前文分析完全一样,照搬前面的图内存中对象如下:
-
b.第二次操作
getPropertyMap()
注意: LogbackMDCAdapter的copy map操作实际发生在getPropertyMap()
方法,而不是getCopyOfContextMap()
方法。
public Map<String, String> getPropertyMap() {
// 通过lastOperation定位到Entry[3], 将Entry[3].value从1更新为2
lastOperation.set(MAP_COPY_OPERATION);
// 返回Entry[0].value,被变量snapshot接收
return copyOnThreadLocal.get();
}
- c.第三次操作执行
put("b", "456")
public class LogbackMDCAdapter implements MDCAdapter {
public void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key cannot be null");
}
// 当前put操作时,从copyOnThreadLocal定位到Entry[0]的value为{a=123}
Map<String, String> oldMap = copyOnThreadLocal.get();
// 从lastOperation定位到Entry[3]的value为2,故返回的lastOp为2;并记录当前操作为写(将Entry[3]的value从2设置为1)
Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);
if (wasLastOpReadOrNull(lastOp) || oldMap == null) {
// lastOp=2, 故进到这个分支;
Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);
// 调用duplicateAndInsertNewMap(oldMap)得到的newMap为{a=123};
// 把当前kv写入newMap, 故最终newMap为{a=123, b=456}
newMap.put(key, val);
} else {
oldMap.put(key, val);
}
}
// 此处传入的oldMap为{a=123}
private Map<String, String> duplicateAndInsertNewMap(Map<String, String> oldMap) {
// new了一个新map对象
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) {
// 将oldMap的内容搬到newMap
newMap.putAll(oldMap);
}
}
// 对ThreadLocal的Entry[0]赋值(Entry[0]的value断开对原来oldMap的引用,指向newMap),最终Entry[0].value为newMap={a=123}
copyOnThreadLocal.set(newMap);
// 返回newMap
return newMap;
}
}
到这里应该就能理解为什么需要记录操作lastOperation以及进行oldMap、newMap的复制操作了。根本作用,就是为了保证在「write -> copy map -> write」这种场景下,每次copy map得到的都是一个当前快照,并且这个快照是不受后面的写操作影响。对照Case2来说,第一次put("a", "123")
操作后, 第二次操作进行copy map得到的快照snapshot为{a=123},第三次操作put("b", "456")
之后,此时mdc里面的内容已经是{a=123, b=456},但是打印snapshot,仍然还是{a=123}没有任何变更。因此lastOperation配合oldMap、newMap的复制操作,做到的就是快照读的效果。假如没有这种设计,在同一个线程里面第一次打印snapshot和第二次打印snapshot,会输出不同的值,就有点薛定谔了!MDC设计最难理解的点就在这里,看懂这个快照读机制才算真正理解了MDC。
四、MDC的局限性
4.1 父子线程数据无法传递
前面已经分析清楚了MDC的源码,归根到底MDC的数据读写都是基于ThreadLocal。如果你熟悉ThreadLocal,肯定知道它还有个子类InheritableThreadLocal
,用来做父子线程的数据传递。由于LogbackMDCAdapter采用的是ThreadLocal而非InheritableThreadLocal,因此存在第一个缺陷就是: 没法做到父子线程的数据传递。写个Case证明下!
- Case3: 证明MDC父子线程数据无法传递
public class MdcTest {
@Test
public void testMdc3() throws InterruptedException {
// 打印出当前绑定的MDCAdapter
System.out.println(MDC.getMDCAdapter().getClass().getName());
MDC.put("foo", "123");
CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
System.out.println(Thread.currentThread() + ": " + MDC.get("foo"));
latch.countDown();
}, "child").start();
latch.await();
System.out.println(Thread.currentThread() + ": " + MDC.get("foo"));
}
}
- 输出:可以看到在child线程里面,从MDC取到的值为null;也就是说父线程main设置到MDC的值,子线程child根本就取不到
ch.qos.logback.classic.util.LogbackMDCAdapter
Thread[child,5,main]: null
Thread[main,5,main]: 123
解决父子线程数据传递手段很简单,将ThreadLocal换成InheritableThreadLocal就可以,InheritableThreadLocal怎么做到的,源码还得看Thread类的init方法,思路就是每当new一个子线程时,就把父线程的inheritableThreadLocals这个ThreadLocaMap复制下,赋值给当前子线程的threadLocals。
public
class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
// 新建一个线程
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
public Thread(String name) {
init(null, null, name, 0);
}
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
Thread parent = currentThread();
// 略去无关代码...
// inheritThreadLocals 传入的默认值就是true
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
// 如果父线程的inheritableThreadLocals不为空,则通过ThreadLocal.createInheritedMap将inheritableThreadLocals复制一份赋值给当前线程(子线程)的threadLocals属性
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// 略去无关代码...
}
}
4.2 线程池使用MDC存在数据传递重复
线程池使用MDC存在数据传递重复,这个问题就比较隐晦了。对照下4.1小节讲到的InheritableThreadLocal的原理,无非就是在线程新创建的时候,对父线程的ThreadLocalMap进行复制操作然后赋值给子线程,也就是说当前仅当线程创建的那个时机,才开始做复制操作。如果线程是通过线程池创建出来,线程循环利用,就只会有一次创建机会,那么就只会在第一次new的时候复制父线程ThreadLocalMap,计时后面父线程后面又更新了999次ThreadLocalMap,子线程都不会再去复制,这样的后果就是子线程从MDC取到的值一直都是第一次复制的值!严格来说,这个锅并非是MDC的,而是ThreadLocal的缺陷!下面还是写个Case证明下
- Case4: 证明InheritableThreadLocal在线程池场景下存在数据传递重复
public class MdcTest {
@Test
public void testInheritableThreadLocal() throws InterruptedException {
final Executor executor = Executors.newFixedThreadPool(1);
final ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();
AtomicInteger times = new AtomicInteger(0);
for (int i = 0; i < 3; i++) {
times.getAndIncrement();
// 这里是在父线程上设置的值
threadLocal.set(i);
String timesStr = "第" + times.get() + "次循环";
System.out.println(timesStr + Thread.currentThread() + ": " + threadLocal.get());
try {
executor.execute(
() -> System.out.println(timesStr + Thread.currentThread() + ":" + " " + threadLocal.get()));
} finally {
threadLocal.remove();
}
}
Thread.sleep(100);
}
}
- 输出
第1次循环,父线程main和子线程pool-1-thread-1从InheritableThreadLocal取到的值都是0,这是对的,证明了InheritableThreadLocal确实能从父线程复制到值;但是从第2次循环开始,父线程的值以及从0变成1、2,而子线程的值始终为0,这就有问题了,证明子线程后续完全不跟随父线程做值的变更!
第1次循环Thread[main,5,main]: 0
第1次循环Thread[pool-1-thread-1,5,main]: 0
第2次循环Thread[main,5,main]: 1
第3次循环Thread[main,5,main]: 2
第2次循环Thread[pool-1-thread-1,5,main]: 0
第3次循环Thread[pool-1-thread-1,5,main]: 0
4.3 如何破局?
针对上面2个缺陷,MDC本身并未给出解决方案。幸运的是阿里巴巴开源的transmittable-thread-local解决了以上个问题,其解决思路:1、针对父子线程数据无法传递问题,TransmittableThreadLocal继承并加强InheritableThreadLocal类;2、针对线程池InheritableThreadLocal数据数据传递存在重复问题,TransmittableThreadLocal提供了TtlRunnable和TtlCallable来修饰提交到线程池的任务,保证每次任务执行前强制从父线程copy下ThreadLocalMap的最新的值。
值得一提的是,除了TransmittableThreadLocal,还有一大神器TtlMDCAdapter,整合TransmittableThreadLocal的能力并直接实现MDCAdapter接口,用起MDC完全感知不到底层原来是TtlMDCAdapter,非常顺滑,再写个Demo展示下其能力!
- TtlMDCAdapter用法
public class MdcTest {
@Test
public void testTTLMDCAdapter() throws InterruptedException {
// MDC绑定TtlMDCAdapter
TtlMDCAdapter.getInstance();
// 修饰线程池
final Executor executor = TtlExecutors.getTtlExecutor(Executors.newFixedThreadPool(1));
AtomicInteger times = new AtomicInteger(0);
for (int i = 0; i < 3; i++) {
times.getAndIncrement();
String key = String.valueOf(i);
// 这里是在父线上设置的值
MDC.put(key, String.valueOf(i));
String timesStr = "第" + times.get() + "次循环";
System.out.println(timesStr + Thread.currentThread() + ": " + MDC.get(key));
try {
executor.execute(
() -> System.out.println(timesStr + Thread.currentThread() + ":" + " " + MDC.get(key)));
} finally {
MDC.remove(key);
}
}
Thread.sleep(100);
}
}
- 输出
父线程main和子线程pool-1-thread-1,三次循环操作,父子线程从MDC读取的值完全一一致!
第1次循环Thread[main,5,main]: 0
第2次循环Thread[main,5,main]: 1
第1次循环Thread[pool-1-thread-1,5,main]: 0
第3次循环Thread[main,5,main]: 2
第2次循环Thread[pool-1-thread-1,5,main]: 1
第3次循环Thread[pool-1-thread-1,5,main]: 2
五、总结
MDC最大的用途在于分布式链路跟踪上,真正会用的人就会知道MDC用起来有多么爽!MDC的源码分析到此终于告一段落,TransmittableThreadLocal和TtlMDCAdapter的源码本文就不再做分析了,好奇的同学可自行研究。最后概括本文主要内容:
- MDC的SPI机制: 以Logback为例,讲述了MDC的SPI机制实现原理。有了这个认识,读者可再去理解SLF4J对具体日志组件的绑定机制,就能做到触类旁通;
- MDC的源码分析: 以源码注释 + 测试Case + 示意图,细致地分析了MDC源码设计思路;
- MDC的2大局限性: 结合测试Case分析、证明MDC存在的问题,提出解决方案,进而引出TransmittableThreadLocal、TtlMDCAdapter两大神器。
全文终~