MDC+Logback+TTL的化學(xué)反應(yīng)

前言

以下代碼為我們產(chǎn)品線目前在使用的日志方案。
由于不是本人的勞動成果,所以會屏蔽部分技術(shù)細(xì)節(jié)。

正文

上下文傳遞

在一次服務(wù)中,也叫一次事務(wù)即常說的TransectionID,是被上下文傳遞的。直到下一次事務(wù)才被更新。在漫長的上下文傳遞過程中這個值就十分容易丟,而且我們要記錄的字段也遠(yuǎn)不止這一項,而且使用傳參的方式做也太過于麻煩了。
因此MDC為“Mapped Diagnostic Context”(映射診斷上下文)就被提出來了。

其基本原理大概是:
MDC包含一個MDCAdapter接口其基本實現(xiàn)類是BasicMDCAdapter,BasicMDCAdapter使用了inheritableThreadLocal。而inheritableThreadLocal又由ThreadLocal改造而來。

那么ThreadLocal是什么呢?
詳見:http://www.itdecent.cn/p/1af4f7582b80
簡單來說:
ThreadLocal是為每一個線程創(chuàng)建一個單獨的變量副本,每個線程都可以獨立地改變自己所擁有的變量副本,而不會影響其他線程所對應(yīng)的副本。
而inheritableThreadLocal在線程切換時(父線程-->子線程),會自動地把這個副本傳遞下去。
這個時候你直接使用MDC.get("key");就像在使用一個普通的Map一樣容易。
因此在日志配置里logback.xml可以配成

<configuration scan="true" scanPeriod="30 seconds" debug="false">
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS}-[%X{TransectionID}] [%t] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>
<root level="INFO" additivity="false" name="cn.itcast.product">
    <appender-ref ref="STDOUT"/>
</root>>
</configuration>

[%X{TransectionID}]代表打印日志時自動打印TransectionID。

但是在多線程實現(xiàn)的時候情況就大有不同了。

多線程情況下使用inheritableThreadLocal的MDC

由于線程池是由IOC容器管理的,一般來說線程池一旦創(chuàng)建就不再銷毀。那么復(fù)用線程池的情況就總是存在。由于不再進(jìn)行初始化,inheritableThreadLocal就無法同步父線程的值。
因此會存在MDC錯亂的現(xiàn)象。
為此阿里巴巴提供了一套解決方案---TransmittableThreadLocal。

TransmittableThreadLocal

詳見:http://www.itdecent.cn/p/e0774f965aa3
TransmittableThreadLocal簡稱TTL。
其原理就是在MDC中替換inheritableThreadLocal為TransmittableThreadLocal從而解決以上問題。
他提供了一個修飾線程池的工具:TtlExecutors.getTtlExecutor(executor)。
具體用法如下:

/**
 * @author AUTO_BEAR
 */
@Configuration
@EnableAsync
public class Threadpool {
    @Bean
    public Executor testExecutor(ThreadPoolConfig config)
    {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(1);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(10);
        executor.setKeepAliveSeconds(5000);
        executor.setThreadNamePrefix("MydearThread-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return TtlExecutors.getTtlExecutor(executor);
    }
}

實現(xiàn)BeanPostProcessor 接口后,便可以在Bean初始化的時候?qū)€程池自動進(jìn)行修飾:

@Configuration
public class NewBeanPostProcessor implements BeanPostProcessor {
    private static final Logger logger=LoggerFactory.getLogger(NewBeanPostProcessor.class);
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if(bean instanceof Executor)
        {
            Executor executor=(Executor)bean;
            if (TtlExecutors.isTtlWrapper(executor))
            {
                logger.info("Executor[{}] is already wrapped by TTLExecutors",executor);
                return executor;
            }
            logger.info("Executor[{}] will be wrapped by TTLExecutors",executor);
            return TtlExecutors.getTtlExecutor(executor);
        }
        return bean;
    }
}

至此問題就基本得到解決了,但是還是會出現(xiàn)一個比較隱蔽的問題,使用TTL+MDC進(jìn)行上下文傳遞時應(yīng)該關(guān)注。

問題復(fù)現(xiàn)

以下代碼隱式使用了ForkJoinPool

  list.parallelStream().forEach(x->map.put(x,printit(x)));

ForkjoinPool是JDK1.8以后提出來的,其執(zhí)行邏輯和其他線程池有點不同,他是竊格瓦拉式的拉風(fēng)但是又是一個十惡不赦的工賊模式---在他空閑的時候他只喜歡偷別人的工作(不是工作成果)。因此這是一個很高效的方法。
我們可以使用原始的方法解決:

 Map<String, String> mdcmap = MDC.getCopyOfContextMap();
  list.parallelStream().forEach(x->map.put(x,printit(x,mdcmap)));
//在printit內(nèi)添加這行代碼
  MDC.setContextMap(mdcmap);

但是我不同意?。?!于是我把代碼改為,每次使用ForkJoinPool我就自己手動New一個出來,因此符合inheritableThreadLocal的邏輯,線程初始化時同步父線程的MDC值。
以下是茴香豆的“囬”的幾種寫法:

1.
ForkJoinPool pool = new ForkJoinPool();
        Map<String, String> mdcmap = MDC.getCopyOfContextMap();
        list.parallelStream().forEach(x->printit(x));
2.
 ForkJoinPool pool = new ForkJoinPool();
       try {
           pool.submit(() -> list.parallelStream().forEach(x -> {
                    this.printit(x);
           })).get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
3.
        ForkJoinPool pool = new ForkJoinPool();
        try {
            for(String s:list){
                SendMessageTask task=new SendMessageTask(s);
                forkJoinPool.execute(task);
            }
            forkJoinPool.awaitTermination(2, TimeUnit.SECONDS);
            forkJoinPool.shutdown();
            if(!forkJoinPool.isShutdown()) {
                forkJoinPool.awaitTermination(5, TimeUnit.SECONDS);
                forkJoinPool.shutdownNow();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
public class SendMessageTask extends RecursiveAction {
  String  s = null;
    public SendMessageTask(String s) {
        this.s = s;
    }
    @Override
    protected void compute() {
    Dosomething....
  }
}

但是這種寫法有一個巨大的危害:我們平時都清楚要避免頻繁創(chuàng)建線程池。為什么呢?https://blog.csdn.net/qq_33744693/article/details/88696584
他告訴我們CPU資源的寶貴,是如何被頻繁創(chuàng)建銷毀所浪費的。
雖然任務(wù)完成了,但是性能也下降了,至少我認(rèn)為是不夠保守的方法,因此:我不同意?。?!

最終解決方案

我們可以看出TTL是管不到那些路邊有事沒事長出來的線程池的,因為畢竟BeanPostProcessor 這個接口是SpringIOC提供的,如果超出IOC的轄區(qū),那么Executor是無法乖乖就范的。因此我們需要把線程池拿到IOC的戶籍本里注冊一下。

那么,答案就藏在了最開始,我們注冊了一個ForkJoinPool的線程池,并供程序使用。

    @Bean
    public Executor myForkJoinPool()
    {
        Executor executor=new ForkJoinPool();
        ExecutorService executor = TtlExecutors.getTtlExecutorService(new ForkJoinPool());
        return executor;
    }

如此他就必然被TTL管理了。然后異步調(diào)用的時候也就可以心安理得地去使用它了:

    @Async("myForkJoinPool")
    public Map<String,String> CALL(List<String> list){
        Map<String ,String > map=new HashMap();
        for(String s:list){
           String rs=printit(s);
           map.put(s,rs);
        }
        return map;
    }

我們不但解決了問題,還把討厭的lambda表達(dá)式給跳過去了,至此本篇結(jié)束。

結(jié)尾

最近工作有點累,但是也有收獲,在前輩的帶領(lǐng)下也學(xué)了不少知識,希望能夠在搬磚這條路上越走越好吧。

我慢慢地對這份工作有所感悟:
軟件業(yè)有一點像運(yùn)輸業(yè),都是服務(wù)業(yè)嘛沒毛病。一開始你可能只是一個跑腿的能把貨物送到就行,慢慢地你開起了車,一開始是開五菱宏光送貨,但是隨著業(yè)務(wù)量的增加你可能接到送幾個老鄉(xiāng)進(jìn)城的需求,因此你要租一輛小轎車,這叫做用戶體驗。
如果讓你運(yùn)鈔,你不但會選一輛裝甲車、運(yùn)鈔車,你還會配備押送人員,你會去考慮存放鈔票的箱子是否結(jié)實可靠,甚至你會躲避人流量多的時間去進(jìn)行運(yùn)鈔。數(shù)據(jù)的可靠性、安全性是一個值得考慮的指標(biāo)。一個大型的系統(tǒng)往往需要一些信息安全的專業(yè)人員。
有時候你會有感而發(fā):飛機(jī)有飛機(jī)的優(yōu)點、摩托車有摩托車的優(yōu)點,時間就是生命!數(shù)據(jù)的時效性、系統(tǒng)的效率也是一個重要的指標(biāo)。

因此任何一個線程池的選型,還有所結(jié)合的List和Map作為入?yún)⒊鰠ⅲㄈ萜鳎┑倪x型都是值得學(xué)習(xí)深究的。
希望下班后別總是玩游戲,還是應(yīng)該看看書,看看別人寫的代碼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容