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() {
        //需要释放,避免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 规范定义的一种过滤器接口,它的主要作用是在 Servlet 容器中对请求和响应进行拦截和处理,实现对请求和响应的预处理、后处理和转换等功能。通过实现 Filter 接口,开发人员可以自定义一些过滤器来实现各种功能,如身份验证、日志记录、字符编码转换、防止 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);
            }

            // 继续将请求传递给下一个过滤器或目标资源(比如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异步任务全局链路
     *
     * @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/8
 * Description: 自定义线程池配置
 * Version:     1.0
 */

@Configuration
public class ThreadPoolConfig {

    @Bean(name = "ownThreadPoolExecutor")
    public ThreadPoolExecutor customThreadPool() {
        int corePoolSize = 5;
        int maximumPoolSize = 10;
        long keepAliveTime = 60;
        TimeUnit unit = TimeUnit.SECONDS;
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(50);
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();

        return new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue,
                threadFactory,
                handler
        );
    }
}

线程包装类

/**
 * Author:      liu_pc
 * Date:        2023/8/8
 * Description: 线程池包装类
 * Version:     1.0
 */

public class ThreadPoolExecutorMdcWrapper  extends ThreadPoolExecutor {

    public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                        BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                        BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }

    public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                        BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    }

    public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                        BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
                                        RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    @Override
    public void execute(@NotNull Runnable task) {
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public <T> Future<T> submit(@NotNull Runnable task, T result) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), result);
    }

    @Override
    public <T> Future<T> submit(@NotNull Callable<T> task) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public Future<?> submit(@NotNull Runnable task) {
        return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
}
/**
 * Author:      liu_pc
 * Date:        2023/8/8
 * Description: 线程池包装类
 * Version:     1.0
 */
public class ThreadMdcUtil {

    /**
     * 当线程中的traceId为空时,赋值新的TraceId
     */
    public static void setTraceIdIfAbsent() {
        if (MDC.get(Constant.TRACE_ID) == null) {
            RequestIdUtil.setRequestId();
        }
    }

    /**
     * 将原始Callable对象封装成新的Callable对象,增加日志上下文信息
     * @param callable 任务线程对象
     * @param context 上下文
     * @return Callable返回值的类型
     * @param <T> 封装后的新的Callable对象
     */
    public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                return callable.call();
            } finally {
                MDC.clear();
            }
        };
    }


    /**
     * 将原始Runnable对象封装成新的Runnable对象,增加日志上下文信息
     * @param runnable 无返回结果的任务线程对象
     * @param context 上下文
     * @return Callable返回值的类型
     */
    public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
        return new Runnable() {
            @Override
            public void run() {
                if (context == null) {
                    MDC.clear();
                } else {
                    MDC.setContextMap(context);
                }
                setTraceIdIfAbsent();
                try {
                    runnable.run();
                } finally {
                    MDC.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";
    }
    private final Executor executor;

    public HelloAsyncService(@Qualifier("ownThreadPoolExecutor") Executor executor) {
        this.executor = executor;
    }

    /**
     * 使用异步数据测试打印日志
     *
     * @param param 请求参数
     */
    public void helloAsync(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) {
            Map<String, String> mainMdcContext = MDC.getCopyOfContextMap();
            CompletableFuture.runAsync(() -> {
                try {
                    MDC.setContextMap(mainMdcContext);
                    //模拟其他业务逻辑处理耗时
                    Thread.sleep(1000);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                logger.info("threadIndex:{} Another thread running", thread);
            }, executor);
        }
    }

HTTP请求接口无法获取到TraceId问题

解决该问题的方法是通过添加适当的HTTP请求工具的拦截器来实现。你可以在文章的末尾附上整个项目的Gitee地址,这样小伙伴们就可以参考我以OKHttp工具为例的代码来实现相同的效果。

代码地址

Gitee代码地址

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

vue elementui v-for 循环el-table-column 第一列数据变到最后一个

这个动态渲染table表格时发现el-table-column 第一列数据变到最后一个 序号被排到后面 代码 修改后 <el-table:data"tableData"tooltip-effect"dark"style"width: 100%"height"500"><template v-for"(item, index) i…

大数据Flink(六十):Flink 数据流和分层 API介绍

文章目录 Flink 数据流和分层 API介绍 一、​​​​​​​​​​​​​​Flink 数据流

【ES】笔记-函数参数默认值

函数参数默认值 ES6 允许给函数参数赋值初始值 1. 形参初始值 具有默认值的参数&#xff0c;一般放到最后 function add(a,b,c10){return abc}let resultadd(1,2);console.log(result);2. 与解构赋值结合 function connect({host"127.0.0.1",username,password,port…

AST入门与实战(三):if节点转switch节点(瑞数5)

原文地址:https://zhuoyue360.com/jsnx/110.html 1. 期望 这是一个瑞数5代解混淆的案例&#xff0c;我们本章节需要做的是把if节点的内容转换成switch-case内容.以此来熟悉AST对JS混淆的对抗. 原始代码: function whileState() {while (1) {aV cA[wU];if (aV < 4) {if (…

前端笔试题1

HTML/CSS 题1&#xff1a; 1&#xff0e;使用CSS 让该节点不可见&#xff0c;方法越多越好。 <div class"hidden">Hi</div> 使用CSS 让节点不可见的方法有以下几种&#xff1a; 把 visibility 属性设置为 hidden&#xff0c;这样元素框不会被绘制&…

nodejs+vue+elementui学生档案信息管理系统_06bg9

利用计算机网络的便利&#xff0c;开发一套基于nodejs的大学生信息管理系统&#xff0c;将会给人们的生活带来更多的便利&#xff0c;而且在经济效益上&#xff0c;也会有很大的便利!这可以节省大量的时间和金钱。学生信息管理系统是学校不可缺少的一个环节&#xff0c;其内容直…

公司自建机房IDC还是选择云服务器?以腾讯云为例

大企业是选择自购服务器自建机房还是使用腾讯云服务器&#xff1f;都说企业上云是趋势&#xff0c;自建机房是一次性支出&#xff0c;上云租赁云服务器等产品需要年年续费&#xff0c;大型企业有必要把数据中心迁移上云吗&#xff1f;腾讯云服务器网想说&#xff0c;自建机房购…

面试八股文Mysql:(2)数据库调优

1. SQL优化很有必要 数据库优化在提升系统性能是很重要的一个方面&#xff0c;不管是MySQL还是MongoDB还是其它的数据库。 SQL优化在提升系统性能中是成本最低 && 优化效果最明显的途径&#xff0c;可以让吞吐量更大&#xff0c;响应速度更快。如果你的团队在SQL优化这…

安防监控视频汇聚平台EasyCVR分发的FLV视频流在VLC中无法播放是什么原因?

众所周知&#xff0c;TSINGSEE青犀视频汇聚平台EasyCVR可支持多协议方式接入&#xff0c;包括主流标准协议国标GB28181、RTSP/Onvif、RTMP等&#xff0c;以及厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等。在视频流的处理与分发上&#xff0c;视频监控…

集合数据类型

非数字型 列表[]&#xff08;其他语言叫数组&#xff09; 注意点&#xff1a;第一个成员的索引编号为0&#xff0c;不能访问不存在的索引编号 # list是列表变量名&#xff0c;列表中有三个成员 list[刘备,曹操,关羽] print(list[0]) print(list[1]) print(list[2]) print(li…

XXLJOB 怎么用

目录 1、数据库执行sql语句&#xff0c;建立表 2、配置Admin &#xff0c;连接xxl_job数据库 3、启动admin&#xff0c;访问 http://localhost:8080/xxl-job-admin 4、需要定时任务的微服务里导入依赖 5、配置yml&#xff08;admin地址&#xff0c;执行器名字 端口&#x…

湘大 XTU OJ 1260 Completed String 题解(非常详细):建立数组下标和数组元素之间的映射关系 ~scanf

一、链接 1260 Completed String 二、题目 题目描述 给一个字符串&#xff0c;请判断字符串是否出现了所有的英文字母&#xff08;不区分大小写&#xff09;。 输入 每行一个只含英文字母的字符串&#xff0c;长度不超过1000。 输出 每行输出一个样例的结果&#xff0c…

Vue.js2+Cesium1.103.0 七、Primitive 绘制航线元素

Vue.js2Cesium1.103.0 七、Primitive 绘制航线元素 用 Primitive 绘制航线元素&#xff0c;包括航点图标&#xff0c;航线线段&#xff0c;线段距离标注&#xff0c;航点序号&#xff0c;海拔标注&#xff0c;总航程等信息。 可同时绘制多条航线&#xff1b;可根据 id 清除指…

基于Vue+wangeditor实现富文本编辑

目录 前言分析实现具体解决的问题有具体代码实现如下效果图总结前言 一个网站需要富文本编辑器功能的原因有很多,以下是一些常见的原因: 方便用户编辑内容:富文本编辑器提供了类似于Office Word的编辑功能,使得那些不太懂HTML的用户也能够方便地编辑网站内容。提高用户体验…

【C++】虚继承(virtual base classes)

【C】虚继承&#xff08;virtual base classes) 文章目录 【C】虚继承&#xff08;virtual base classes)1. 使用原因2. 解决方法3. 例题练习 1. 使用原因 在多重继承(Multiple Inheritance) 的情况下&#xff0c;尤其是菱形继承时&#xff0c;容易出现问题&#xff0c;关于菱…

SpringSecurity 详解(通俗易懂)

SpringSecurity 详解 1、SpringSecurity讲解1.1、SpringSecurity完整流程1.2、认证流程 2、登录&#xff0c;退出&#xff0c;注册_分析说明2.1、登录2.2、校验2.3、退出2.4、注册2.5、SecurityContextHolder说明 3、代码实现3.1、引入依赖3.2、登录 退出 注册3.2.1、SpringSec…

Xcode 基座打包

Xcode基座打包-APP更新版本内容无效 问题&#xff1a;解决&#xff1a; 问题&#xff1a; 使用xcode基座打包之后&#xff0c;上传到appstore进行提审发布。 用户在appstore商城进行更新下载&#xff0c;打开更新后的APP发现版本号是最新的&#xff0c;APP里面的其他内容还是上…

HTML5基础

1、HTML5概述 2014年10月28日&#xff0c;W3C&#xff08;world wide web consortium&#xff0c;万维网联盟&#xff09;的HTML工作组发布了HTML5的正式推荐标准。HTML5作为构建开放Web平台的核心&#xff0c;增加了支持Web应用的许多新特性&#xff0c;以及更符合开发者使用…

Arcgis地图实战二:地图实时轨迹展示

1.最终效果预览 2.定时器执行方法 进入页面执行执行器 this.locationInterval setInterval(() > {this.getCurrentPosition();}, this.conf.LocateInterval);离开页面销毁 clearInterval(this.locationInterval);this.conf.LocateInterval为获取的数据同步中的定时器间隔…

你的服务器安全吗?--服务器防渗透

1、概述 在本人所处的公司的服务器正式遭到黑客攻击之前&#xff0c;一直都以为 黑客 是个遥不可及的词&#xff0c;直到真正成为了受害者时&#xff0c;才猛然意识到安全的重要性。有一些基本经验和心得总结出来&#xff0c;和同行分享一下吧。 2、暴破手段 最粗暴的黑客行为…