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.mzph.cn/news/28509.shtml

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

相关文章

XML基础知识讲解

文章目录 1. xml简介2. xml快速入门3. xml的元素(标签)定义4. xml标签的命名规范5. xml的属性定义和注释6. 转义字符7. CDATA区8. xml的处理指令9. xml的约束 1. xml简介 XML&#xff08;eXtensible Markup Language&#xff09;是一种用于描述数据的标记语。 它以纯文本的方…

使用 Etcher 制作U盘系统盘

Etcher 资料&#xff1a; https://github.com/balena-io/etcher/blob/master/SUPPORT.md

司徒理财:8.9黄金早盘低多,黄金走势分析操作建议

黄金早盘1923现价做多看涨&#xff0c;黄金将开启反弹&#xff0c;低多&#xff0c;上方压力1945一线&#xff01;黄金4小时周期下跌macd指标已经背离&#xff0c;昨日虽然破位新低&#xff0c;但没有延续&#xff0c;形成小双底结构&#xff01;弱不再弱必转强&#xff01;今日…

基于 eclipse-temurin 镜像部署spring boot 应用

基于 eclipse-temurin 镜像部署spring boot 应用 使用场景示例项目 使用场景 在CI流程中&#xff0c;一般都会集成 打包&#xff0c;构建镜像&#xff0c;分发&#xff0c;启动容器之类的流程&#xff1b; 这里提供一个示例&#xff0c;进攻参考 示例项目 项目结构如下 run…

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

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

加密和安全

加密和安全 一.安全机制 安全攻击的几种典型方式&#xff1a; STRIDE Spoofing 假冒 Tampering 篡改 Repudiation 否认 Information Disclosure 信息泄漏 Denial of Service 拒绝服务 Elevation of Privilege 提升…

从少年变成恶龙的平台经济

点击文末“阅读原文”即可参与节目互动 剪辑、音频 / 卷圈 运营 / SandLiu 卷圈 监制 / 姝琦 封面 / 姝琦midjourney 产品统筹 / bobo 录音间 / 声湃轩北京站 东方甄选和抖音的“矛盾”再一次暴露出平台经济与入驻者之间微妙的关系。 平台经济&#xff0c;从一个引领时代…

OpenStreetMap 上基于A*搜索算法的C ++路线规划项目

引言 在现代的地理信息系统&#xff08;GIS&#xff09;中&#xff0c;路线规划是一个重要的组成部分。它涉及到从一个地点到另一个地点的最优路径的确定。在这篇文章中&#xff0c;我们将探讨如何在OpenStreetMap数据上实现一个基于A*搜索算法的C路线规划项目。 OpenStreetM…

无涯教程-Perl - bless函数

描述 此函数告诉REF引用的实体,它现在是CLASSNAME包中的对象,如果省略CLASSNAME,则为当前包中的对象。建议使用bless的两个参数形式。 语法 以下是此函数的简单语法- bless REF, CLASSNAMEbless REF返回值 该函数返回对祝福到CLASSNAME中的对象的引用。 例 以下是显示其…

Proxy与Reflect

Proxy 构造函数 Proxy 对象用于创建一个对象的代理&#xff0c;从而实现基本操作的拦截和自定义&#xff08;如属性查找、赋值、枚举、函数调用等&#xff09;。 语法&#xff1a;new Proxy(target, handler) 参数&#xff1a; target&#xff1a;要使用 Proxy 包装的目标…

SQL必知必会读书笔记

文章目录 **不同的DB语法格式不尽相同**第一课 了解SQL术语 第二课 检索数据语法格式检索列检索唯一不同值限制结果&#xff08;数量&#xff09; 第三课 排序检索数据使用说明 第四课 过滤数据WHERE子句操作符 第五课 高级数据过滤1、组合WHERE子句2、IN操作符3、NOT操作符 第…

【数据结构与算法——TypeScript】哈希表

【数据结构与算法——TypeScript】 哈希表(HashTable) 哈希表介绍和特性 哈希表是一种非常重要的数据结构&#xff0c;但是很多学习编程的人一直搞不懂哈希表到底是如何实现的。 在这一章节中&#xff0c;我门就一点点来实现一个自己的哈希表。通过实现来理解哈希表背后的原理…

前后端交互开发模式yapi使用

接手一个项目组,采用前后端开发模式分离,经过一阵子了解后,发现存在前后端配合不顺畅的情况,存在如下两个问题, 一:没有接口协议,前端开发时先用占位符,等后端开发协议出来后替换,影响效率。 二:前端开发好了, 后端没开发好,前端只能等待后端开发好。 做为一个团队技…

记录uniapp 滚动后溢出显示空白的办法

写了一个横向滚动&#xff0c;超出可视区域图片空白&#xff0c;上下滚动页面可视区域图片显示&#xff0c;不可见区域滚动出来变成空白 错误css如下 width: 678rpx;height: 264rpx;background: #ffffff;border-radius: 16rpx;margin: 64rpx 18rpx 10rpx 18rpx;overflow-y: hid…

Zabbix网络拓扑配置

一、简介 网络拓扑功能是一项非常重要的功能&#xff0c;它可以直观展示网络设备主机状态及端口传输速率等指标信息&#xff0c;帮助运维人员快速发现和定位故障问题&#xff1b;Zabbix同样配备了强大的网络拓扑功能&#xff0c;如何使用Zabbix拓扑图功能创建一个公司网络拓扑…

11_Pulsar Adaptors适配器、kafka适配器、Spark适配器

2.3. Pulsar Adaptors适配器 2.3.1.kafka适配器 2.3.2.Spark适配器 2.3. Pulsar Adaptors适配器 2.3.1.kafka适配器 Pulsar 为使用 Apache Kafka Java 客户端 API 编写的应用程序提供了一个简单的解决方案。 在生产者中, 如果想不改变原有kafka的代码架构, 就切换到Pulsar的…

FreeRTOS通过消息队列实现串口命令解析(串口中断)

作者&#xff1a;Jack_G 时间&#xff1a;2023.08.08 版本&#xff1a;V1.0 上次修改时间&#xff1a; 环境&#xff1a; \quad \quad \quad \quad STM32Cube MX V6.8.1 \quad \quad \quad \quad STM32CubeH7 Firmware Package V1.11.0 / 04-Nov-2022 \quad \quad \quad \qu…

[SQL智慧航行者] - 用户购买商品推荐

话不多说, 先看数据表信息. 数据表信息: employee 表, 包含所有员工信息, 每个员工有其对应的 id, salary 和 departmentid. --------------------------------- | id | name | salary | departmentid | --------------------------------- | 1 | Joe | 70000 | 1 …

抖音的竞争对手?Meta计划人工智能聊天机器人增加社交媒体数量

在来自抖音的竞争中&#xff0c;Meta着眼于用户参与的下一个前沿。 报道&#xff0c;Meta正在开发一系列具有不同个性的人工智能聊天机器人&#xff0c;此举旨在增加用户在脸书和Instagram等社交平台上的参与度金融时报和边缘。这些聊天机器人被Meta staff称为“personas ”,将…

LabVIEW开发高压配电设备振动信号特征提取与模式识别

LabVIEW开发高压配电设备振动信号特征提取与模式识别 矿用高压配电设备是井下供电系统中的关键设备之一&#xff0c;肩负着井下供配电和供电安全的双重任务&#xff0c;其工作状态直接影响着井下供电系统的安全性和可靠性。机械故障占配电总故障的70%。因此&#xff0c;机械故…