前言
以下代碼為我們產(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)該看看書,看看別人寫的代碼。