SpringBoot?MDC全局鏈路最新完美解決方案
需求
在訪問量較大的分布式系統(tǒng)中,時時刻刻在打印著巨量的日志,當我們需要排查問題時,需要從巨量的日志信息中找到本次排查內(nèi)容的日志是相對復(fù)雜的,那么,如何才能使日志看起來邏輯清晰呢?如果每一次請求都有一個全局唯一的id,當我們需要排查時,根據(jù)其他日志打印關(guān)鍵字定位到對應(yīng)請求的全局唯一id,再根據(jù)id去搜索、篩選即可找到對應(yīng)請求全流程的日志信息。接下來就是需要找一種方案,可以生成全局唯一id和在不同的線程中存儲這個id。
解決方案
LogBack這個日志框架提供了MDC( Mapped Diagnostic Context,映射調(diào)試上下文 ) 這個功能,MDC可以理解為與線程綁定的數(shù)據(jù)存儲器。數(shù)據(jù)可以被當前線程訪問,當前線程的子線程會繼承其父線程中MDC的內(nèi)容。MDC 在 Spring Boot 中的作用是為日志事件提供上下文信息,并將其與特定的請求、線程或操作關(guān)聯(lián)起來。通過使用 MDC,可以更好地理解和分析日志,并在多線程環(huán)境中確保日志的準確性和一致性。此外,MDC 還可以用于日志審計、故障排查和跟蹤特定操作的執(zhí)行路徑。
代碼
實現(xiàn)日志打印全局鏈路唯一id的功能,需要三個信息:
- 全局唯一ID生成器
- 請求攔截器
- 自定義線程池(可選)
- 日志配置
全局唯一ID生成器
生成器可選方案有:
UUID,快速隨機生成、極小概率重復(fù)Snowflake,有序遞增時間戳
雪花算法(Snowflake)更適用于需要自增的業(yè)務(wù)場景,如數(shù)據(jù)庫主鍵、訂單號、消息隊列的消息ID等, 時間戳一般是微秒級別,極限情況下,一微秒內(nèi)可能同時多個請求進來導(dǎo)致重復(fù)。系統(tǒng)時鐘回撥時,UUID可能會重復(fù),但是一般不會出現(xiàn)該情況,因此UUID這種方案的缺點可以接受,本案例使用UUID方案。
/**
* 全局鏈路id生成工具類
*
* @author Ltx
* @version 1.0
*/
public class RequestIdUtil {
public RequestIdUtil() {
}
public static void setRequestId() {
//往MDC中存入UUID唯一標識
MDC.put(Constant.TRACE_ID, UUID.randomUUID().toString());
}
public static void setRequestId(String requestId) {
MDC.put(Constant.TRACE_ID, requestId);
}
public static String getRequestId() {
return MDC.get(Constant.TRACE_ID);
}
public static void clear() {
//需要釋放,避免OOM
MDC.clear();
}
}/**
* Author: liu_pc
* Date: 2023/8/8
* Description: 常量定義類
* Version: 1.0
*/
public class Constant {
/**
* 全局唯一鏈路id
*/
public final static String TRACE_ID = "traceId";
}自定義全局唯一攔截器

Filter是Java Servlet 規(guī)范定義的一種過濾器接口,它的主要作用是在 Servlet 容器中對請求和響應(yīng)進行攔截和處理,實現(xiàn)對請求和響應(yīng)的預(yù)處理、后處理和轉(zhuǎn)換等功能。通過實現(xiàn) Filter 接口,開發(fā)人員可以自定義一些過濾器來實現(xiàn)各種功能,如身份驗證、日志記錄、字符編碼轉(zhuǎn)換、防止 XSS 攻擊、防止 CSRF 攻擊等。那么這里我們使用它對請求做MDC賦值處理。
@Component
public class RequestIdFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException{
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String requestId = httpServletRequest.getHeader("requestId");
if (StringUtils.isBlank(requestId)) {
RequestIdUtil.setRequestId();
} else {
RequestIdUtil.setRequestId(requestId);
}
// 繼續(xù)將請求傳遞給下一個過濾器或目標資源(比如Controller)
filterChain.doFilter(servletRequest, servletResponse);
} finally {
RequestIdUtil.clear();
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void destroy() {
Filter.super.destroy();
}
} /**
* 測試MDC異步任務(wù)全局鏈路
*
* @param param 請求參數(shù)
* @return new String Info
*/
public String test(String param) {
logger.info("測試MDC test 接口開始,請求參數(shù):{}", param);
String requestId = RequestIdUtil.getRequestId();
logger.info("MDC RequestId :{}", requestId);
return "hello";
}日志配置
輸出到控制臺:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 配置輸出到控制臺(可選輸出到文件) -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- 配置日志格式 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %mdc %msg%n</pattern>
</encoder>
</appender>
<!-- 配置根日志記錄器 -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
<!-- 配置MDC -->
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
<resetJUL>true</resetJUL>
</contextListener>
<!-- 配置MDC插件 -->
<conversionRule conversionWord="%mdc" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
</configuration>
輸出到文件:
<configuration>
<!-- 配置輸出到文件 -->
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<!-- 指定日志文件路徑和文件名 -->
<file>/Users/liu_pc/Documents/code/mdc_logback/logs/app.log</file>
<encoder>
<!-- 配置日志格式 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %mdc %msg%n</pattern>
</encoder>
</appender>
<!-- 配置根日志記錄器 -->
<root level="INFO">
<appender-ref ref="FILE"/>
</root>
<!-- 其他配置... -->
</configuration>
功能實現(xiàn)。
子線程獲取traceId問題
使用多線程時,子線程打印日志拿不到traceId。如果在子線程中獲取traceId,那么就相當于往各自線程中的MDC賦值了traceId,會導(dǎo)致子線程traceId不一致的問題。
public void wrongHelloAsync(String param) {
logger.info("helloAsync 開始執(zhí)行異步操作,請求參數(shù):{}", param);
List<Integer> simulateThreadList = new ArrayList<>(5);
for (int i = 0; i <= 5; i++) {
simulateThreadList.add(i);
}
for (Integer thread : simulateThreadList) {
CompletableFuture.runAsync(() -> {
//在子線程中賦值
String requestId = RequestIdUtil.getRequestId();
logger.info("子線程信息:{},traceId:{} ", thread, requestId);
}, executor);
}
}
}子線程獲取traceId方案
使用子線程時,可以使用自定義線程池重寫部分方法,在重寫的方法中獲取當前MDC數(shù)據(jù)副本,再將副本信息賦值給子線程的方案。
/**
* Author: liu_pc
* Date: 2023/8/7
* Description: 自定義異步線程池配置
* Version: 1.0
*/
@Configuration
@EnableAsync
public class AsyncConfiguration implements AsyncConfigurer {
private final Logger logger = LoggerFactory.getLogger(AsyncConfiguration.class);
private final TaskExecutionProperties taskExecutionProperties;
public AsyncConfiguration(TaskExecutionProperties taskExecutionProperties) {
this.taskExecutionProperties = taskExecutionProperties;
}
@Override
@Bean(name = "taskExecutor")
public Executor initAsyncExecutor() {
logger.debug("Creating Async Task Executor");
ThreadPoolTaskExecutor executor = new MdcThreadPoolTaskExecutor();
executor.setCorePoolSize(taskExecutionProperties.getPool().getCoreSize());
executor.setMaxPoolSize(taskExecutionProperties.getPool().getMaxSize());
executor.setQueueCapacity(taskExecutionProperties.getPool().getQueueCapacity());
executor.setThreadNamePrefix(taskExecutionProperties.getThreadNamePrefix());
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new SimpleAsyncUncaughtExceptionHandler();
}
}/**
* Author: liu_pc
* Date: 2023/8/7
* Description: 自定義攜帶MDC信息線程池
* Version: 1.0
*/
public class MdcThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
@Override
public void execute(@Nonnull Runnable task) {
Map<String, String> copyOfContextMap = MDC.getCopyOfContextMap();
super.execute(
() -> {
if (Objects.nonNull(copyOfContextMap)) {
String requestId = RequestIdUtil.getRequestId();
if (StringUtils.isBlank(requestId)) {
copyOfContextMap.put("traceId", UUID.randomUUID().toString());
}
//主線程MDC賦值子線程
MDC.setContextMap(copyOfContextMap);
} else {
RequestIdUtil.setRequestId();
}
try {
task.run();
} finally {
RequestIdUtil.clear();
}
}
);
}
}測試代碼:
/**
* 測試MDC異步任務(wù)全局鏈路
*
* @param param 請求參數(shù)
* @return new String Info
*/
public String test(String param) {
logger.info("測試MDC test 接口開始,請求參數(shù):{}", param);
String requestId = RequestIdUtil.getRequestId();
logger.info("MDC RequestId :{}", requestId);
helloAsyncService.helloAsync(param, requestId);
return "hello";
} /**
* 使用異步數(shù)據(jù)測試打印日志
*
* @param param 請求參數(shù)
* @param requestId 全局唯一id
*/
@Async("taskExecutor")
public void helloAsync(String param, String requestId) {
logger.info("helloAsync 開始執(zhí)行異步操作,請求參數(shù):{}", param);
List<Integer> simulateThreadList = new ArrayList<>(5);
for (int i = 0; i <= 5; i++) {
simulateThreadList.add(i);
}
for (Integer thread : simulateThreadList) {
CompletableFuture.runAsync(() -> {
//在子線程中賦值
RequestIdUtil.setRequestId(requestId);
logger.info("子線程信息:{},traceId:{} ", thread, requestId);
}, executor);
}
}
MDC原理
到此這篇關(guān)于SpringBoot MDC全局鏈路解決方案的文章就介紹到這了,更多相關(guān)SpringBoot MDC全局鏈路內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java開發(fā)gui教程之jframe監(jiān)聽窗體大小變化事件和jframe創(chuàng)建窗體
這篇文章主要介紹了java開發(fā)gui教程中jframe監(jiān)聽窗體大小變化事件和jframe創(chuàng)建窗體的示例,需要的朋友可以參考下2014-03-03
SpringBoot3實戰(zhàn)集成mzt-biz-log實現(xiàn)日志記錄
本文主要介紹了SpringBoot3實戰(zhàn)集成mzt-biz-log實現(xiàn)日志記錄,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2026-04-04
關(guān)于各種排列組合java算法實現(xiàn)方法
這篇文章介紹了幾種用JAVA實現(xiàn)的排列組合算法,有需要的朋友可以參考一下2013-06-06
IDEA在SpringBoot項目使用Maven打包后jar包太小問題及解決
這篇文章主要介紹了IDEA在SpringBoot項目使用Maven打包后jar包太小問題及解決,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-04-04
Java中利用POI優(yōu)雅的導(dǎo)出Excel文件詳解
這篇文章主要給大家介紹了關(guān)于Java中如何利用POI優(yōu)雅的導(dǎo)出Excel文件的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家學習或者使用Java具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧2020-05-05

