SpringBoot MDC全局链路解决方案

需求

在访问量较大的分布式系统中,时时刻刻在打印着巨量的日志,当我们需要排查问题时,需要从巨量的日志信息中找到本次排查内容的日志是相对复杂的,那么,如何才能使日志看起来逻辑清晰呢?如果每一次请求都有一个全局唯一的id,当我们需要排查时,根据其他日志打印关键字定位到对应请求的全局唯一id,再根据id去搜索、筛选即可找到对应请求全流程的日志信息。接下来就是需要找一种方案,可以生成全局唯一id和在不同的线程中存储这个id。

解决方案

LogBack这个日志框架提供了MDC( Mapped Diagnostic Context,映射调试上下文 ) 这个功能,MDC可以理解为与线程绑定的数据存储器。数据可以被当前线程访问,当前线程的子线程会继承其父线程中MDC的内容。MDC 在 Spring Boot 中的作用是为日志事件提供上下文信息,并将其与特定的请求、线程或操作关联起来。通过使用 MDC,可以更好地理解和分析日志,并在多线程环境中确保日志的准确性和一致性。此外,MDC 还可以用于日志审计、故障排查和跟踪特定操作的执行路径。

代码

实现日志打印全局链路唯一id的功能,需要三个信息:

  • 全局唯一ID生成器
  • 请求拦截器
  • 自定义线程池(可选)
  • 日志配置
全局唯一ID生成器

生成器可选方案有:

  • UUID,快速随机生成、极小概率重复
  • Snowflake,有序递增
  • 时间戳

雪花算法(Snowflake)更适用于需要自增的业务场景,如数据库主键、订单号、消息队列的消息ID等, 时间戳一般是微秒级别,极限情况下,一微秒内可能同时多个请求进来导致重复。系统时钟回拨时,UUID可能会重复,但是一般不会出现该情况,因此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() {//需要释放,避免OOMMDC.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 规范定义的一种过滤器接口,它的主要作用是在 Servlet 容器中对请求和响应进行拦截和处理,实现对请求和响应的预处理、后处理和转换等功能。通过实现 Filter 接口,开发人员可以自定义一些过滤器来实现各种功能,如身份验证、日志记录、字符编码转换、防止 XSS 攻击、防止 CSRF 攻击等。那么这里我们使用它对请求做MDC赋值处理。

@Component
public class RequestIdFilter implements Filter {@Overridepublic 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);}// 继续将请求传递给下一个过滤器或目标资源(比如Controller)filterChain.doFilter(servletRequest, servletResponse);} finally {RequestIdUtil.clear();}}@Overridepublic void init(FilterConfig filterConfig) throws ServletException {Filter.super.init(filterConfig);}@Overridepublic void destroy() {Filter.super.destroy();}
}
    /*** 测试MDC异步任务全局链路** @param param 请求参数* @return new String Info*/public String test(String param) {logger.info("测试MDC test 接口开始,请求参数:{}", 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>

功能实现。

子线程获取traceId问题

使用多线程时,子线程打印日志拿不到traceId。如果在子线程中获取traceId,那么就相当于往各自线程中的MDC赋值了traceId,会导致子线程traceId不一致的问题。

    public void wrongHelloAsync(String param) {logger.info("helloAsync 开始执行异步操作,请求参数:{}", 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数据副本,再将副本信息赋值给子线程的方案。

/*** 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;}@Overridepublic AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {return new SimpleAsyncUncaughtExceptionHandler();}
}
/*** Author:      liu_pc* Date:        2023/8/7* Description: 自定义携带MDC信息线程池* Version:     1.0*/
public class MdcThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {@Overridepublic 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异步任务全局链路** @param param 请求参数* @return new String Info*/public String test(String param) {logger.info("测试MDC test 接口开始,请求参数:{}", param);String requestId = RequestIdUtil.getRequestId();logger.info("MDC RequestId :{}", requestId);helloAsyncService.helloAsync(param, requestId);return "hello";}
    /*** 使用异步数据测试打印日志** @param param     请求参数* @param requestId 全局唯一id*/@Async("taskExecutor")public void helloAsync(String param, String requestId) {logger.info("helloAsync 开始执行异步操作,请求参数:{}", 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原理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/81432.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

风控安全产品系统设计的一些思考

背景 本篇文章会从系统架构设计的角度&#xff0c;分享在对业务安全风控相关基础安全产品进行系统设计时遇到的问题难点及其解决方案。 内容包括三部分&#xff1a;&#xff08;1&#xff09;风控业务架构&#xff1b;&#xff08;2&#xff09;基础安全产品的职责&#xff1…

【网络安全】网络安全威胁实时地图 - 2023

文章目录 [TOC] ① 360 安全大脑360 APT全景雷达 ② 瑞星杀毒瑞星云安全瑞星网络威胁态势感知平台 ③ 比特梵德 Bitdefender④ 飞塔防火墙 FortiGuard⑤ 音墙网络 Sonicwall⑥ 捷邦 Check Point⑦ AO卡巴斯基实验室全球模拟隧道模拟 ⑧ 数字攻击地图⑨ Threatbutt互联网黑客攻击…

springboot集成分布式任务调度系统xxl-job(调度器和执行器)

一、部署xxl-job服务端 下载xxl-job源码 下载地址&#xff1a; https://gitee.com/xuxueli0323/xxl-job 二、导入项目、创建xxl_job数据库、修改配置文件为自己的数据库 三、启动项目、访问首页 访问地址&#xff1a; http://localhost:8080/xxl-job-admin/ 账号&#xff1…

Android AccessibilityService研究

AccessibilityService流程分析 AccessibilityService开启方式AccessibilityService 开启原理 AccessibilityService开启方式 . 在Framework里直接添加对应用app 服务component。 loadSetting(stmt, Settings.Secure.ACCESSIBILITY_ENABLED,1); loadSetting(stmt, Settings.Se…

vue去掉所有输入框两边空格,封装指令去空格,支持Vue2和Vue3,ElementUI Input去空格

需求背景 就是页面很多表单输入框&#xff0c;期望在提交的时候&#xff0c;都要把用户两边的空格去掉 ❌使用 vue 的指令 .trim 去掉空格 中间会输入不了空格&#xff0c; 比如我想输入 你好啊 中国, 这中间的空格输入不了&#xff0c;只能变成 你好啊中国 ❌在提交的时候使用…

Spring Boot日志文件

文章目录 &#x1f9ca;1.日志有什么作用&#x1f9ca;2.认识日志&#x1f9ca;3.自定义打印日志&#x1f95d;3.1得到日志对象&#x1f95d;3.2利用日志对象的方法打印日志&#x1f95d;3.3日志格式说明 &#x1f9ca;4.日志级别&#x1f95d;4.1 认识日志级别&#x1f95d;4.…

super父类 事物

一个没有事物的方法。 调用他的父类里有事物的方法。 无论this 和 super 都会让父类事物方法没有事物。 如果写了super.class 文件里面&#xff0c;就是super调用。 如果没写&#xff0c;就是this调用&#xff0c;坑爹 测试&#xff0c;把父类注入&#xff0c;事物才生效。

Javaweb学习(2)

Javaweb学习 一、Maven1.1 Maven概述1.2 Maven简介1.3、Maven基本使用1.4、IDEA配置Maven1.6、依赖管理&依赖范围 二、MyBatis2.1 MyBatis简介2.2 Mybatis快速入门2.3、解决SQL映射文件的警告提示2.4、Mapper代理开发 三、MyBaits核心配置文件四、 配置文件的增删改查4.1 M…

python爬虫2:requests库-原理

python爬虫2&#xff1a;requests库-原理 前言 ​ python实现网络爬虫非常简单&#xff0c;只需要掌握一定的基础知识和一定的库使用技巧即可。本系列目标旨在梳理相关知识点&#xff0c;方便以后复习。 目录结构 文章目录 python爬虫2&#xff1a;requests库-原理1. 概述2. re…

解决Error running XXXApplicationCommand line is too long.报错

测试IDEA版本&#xff1a;2019.2.4 &#xff0c;2020.1.3 文章目录 一. 问题场景二. 报错原因2.1 为什么命令行过长会导致这种问题? 三. 解决方案3.1 方案一3.2 方案二 一. 问题场景 当我们从GitHub或公司自己搭建的git仓库上拉取项目代码时&#xff0c;会出现以下错误 报错代…

以技术驱动反欺诈,Riskified 为企业出海保驾护航

如今&#xff0c;全球对于线上消费的需求日益增长&#xff0c;各类新型支付方式也层出不穷。在国内&#xff0c;线上支付有着较为完善的法律及监管条例&#xff0c;格局基本已定型。但对于出海商家而言&#xff0c;由于不同国家和地区的支付规则和监管机制不同&#xff0c;跨境…

实现 Notification 通知

如图这种效果 可以使用 Notification API来进行实现 代码如下 注意&#xff1a;一定要用服务端打开。不然不会弹出来。vscode可以安装 live Serve 插件服务端打开 <!DOCTYPE html> <html lang"zh"><head><meta charset"UTF-8">…

Mac强制停止应用

有时候使用Mac的时候&#xff0c;某个应用卡住了&#xff0c;但是肯定不能因为一个应用卡住了&#xff0c; 就将电脑重启吧&#xff0c;所以只需要单独停止该应用即可&#xff0c;使用快捷键optioncommandesc就会出现强制停止的界面&#xff0c;选择所要停止的应用&#xff0c;…

【css】属性选择器

有些场景中需要在相同元素中获取具有特定属性的元素&#xff0c;比如同为input&#xff0c;type属性有text、button&#xff0c;可以通过属性选择器设置text和button的不同样式。 代码&#xff1a; <style> input[typetext] {width: 150px;display: block;margin-bottom…

64 # 实现一个 http-server

准备工作 上一节实现了通过 commander 的配置获取到用户的参数&#xff0c;下面完成借用 promise 写成类的方法一节没有完成的任务&#xff0c;实现一个 http-server&#xff0c;https://www.npmjs.com/package/http-server&#xff0c;http-server 是一个简单的零配置命令行静…

【每日一题】—— B. Maximum Rounding(Codeforces Round 891 (Div. 3))

&#x1f30f;博客主页&#xff1a;PH_modest的博客主页 &#x1f6a9;当前专栏&#xff1a;每日一题 &#x1f48c;其他专栏&#xff1a; &#x1f534; 每日反刍 &#x1f7e1; C跬步积累 &#x1f7e2; C语言跬步积累 &#x1f308;座右铭&#xff1a;广积粮&#xff0c;缓称…

opencv基础-34 图像平滑处理-2D 卷积 cv2.filter2D()

2D卷积是一种图像处理和计算机视觉中常用的操作&#xff0c;用于在图像上应用滤波器或卷积核&#xff0c;从而对图像进行特征提取、平滑处理或边缘检测等操作。 在2D卷积中&#xff0c;图像和卷积核都是二维的矩阵或数组。卷积操作将卷积核在图像上滑动&#xff0c;对每个局部区…

【图像去噪】基于混合自适应(EM 自适应)实现自适应图像去噪研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

怎么加密文件夹才更安全?安全文件夹加密软件推荐

文件夹加密可以让其中数据更加安全&#xff0c;但并非所有加密方式都能够提高极高的安全强度。那么&#xff0c;怎么加密文件夹才更安全呢&#xff1f;下面我们就来了解一下那些安全的文件夹加密软件。 文件夹加密超级大师 如果要评选最安全的文件夹加密软件&#xff0c;那么文…

ATFX汇市:美联储年内或仍将加息依次,美指向下空间不大

环球汇市行情摘要—— 昨日&#xff0c;美元指数上涨0.08%&#xff0c;收盘在102.08点&#xff0c; 欧元贬值0.07%&#xff0c;收盘价1.1003点&#xff1b; 日元贬值0.51%&#xff0c;收盘价142.47点&#xff1b; 英镑升值0.28%&#xff0c;收盘价1.2784点&#xff1b; 瑞…