SpringBoot MDC全局链路解决方案

这篇具有很好参考价值的文章主要介绍了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";
}
自定义全局唯一拦截器

SpringBoot MDC全局链路解决方案,思考,笔记,Spring框架,spring boot,java,spring

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>

SpringBoot MDC全局链路解决方案,思考,笔记,Spring框架,spring boot,java,spring

输出到文件:

<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>

SpringBoot MDC全局链路解决方案,思考,笔记,Spring框架,spring boot,java,spring

功能实现。

子线程获取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);
        }
    }

SpringBoot MDC全局链路解决方案,思考,笔记,Spring框架,spring boot,java,spring

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

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

SpringBoot MDC全局链路解决方案,思考,笔记,Spring框架,spring boot,java,spring

代码地址

Gitee代码地址文章来源地址https://www.toymoban.com/news/detail-633780.html

到了这里,关于SpringBoot MDC全局链路解决方案的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • #clickid#CID#全新小程序链路CID/clickid解决方案,合规、完美防阿里封禁

    2022年6月及11月,阿里对淘宝联盟链路cid、clickid方案服务商,使用惩罚系统,对调用订单明细的淘客进行了限流措施,绝大多数服务商都退出了该链路。 2022年10月份开始,多数小程序链路的cid、clickid方案服务商,被阿里警告下架小程序,尤其在双11节点,多数Cid小程序被下架

    2024年02月09日
    浏览(44)
  • 长视频自动化摘要笔记完整工作流;腾讯云发布AIGC全链路内容安全解决方案

    🦉 AI新闻 🚀 腾讯云发布AIGC全链路内容安全解决方案,助力企业护航生成式人工智能健康发展 摘要 :腾讯云公布MaaS能力全景图,提供AIGC全链路内容安全解决方案,覆盖从模型训练到内容生成再到事后运营全过程的内容安全建设。解决方案包含审校、安全专家、机器审核和

    2024年02月13日
    浏览(50)
  • android 12.0状态栏高度为0时,系统全局手势失效的解决方案

    在12.0的framework 系统全局手势事件也是系统非常重要的功能,但是当隐藏状态栏, 当把状态栏高度设置为0时,这时全局手势事件失效,这就要从系统手势滑动流程来分析 看怎么样实现系统手势功能的,然后根据功能做修改

    2024年02月07日
    浏览(38)
  • MDC轻量化日志链路跟踪的若干种应用场景

    \\\"If debugging is the process of removing software bugs, then programming must be the process of putting them in.\\\" - Edsger Dijkstra “如果调试是消除软件Bug的过程,那么编程就是产出Bug的过程。” —— 艾兹格·迪杰斯特拉 目录 0x00 大纲 0x01 前言 0x02 应用场景 CLI 程序 Web 应用(服务端) Web 应用(客户端

    2023年04月21日
    浏览(48)
  • hutool工具包 中的雪花算法Snowflake 获取Long类型id 或者String 类型id(全局唯一id解决方案)

    1.引入pom依赖 2.源码 3. 注入 使用 4优缺点:

    2024年02月14日
    浏览(44)
  • Spring Boot+MDC 实现全链路调用日志跟踪,非常优雅!​

    写在前面 通过本文将了解到什么是MDC、MDC应用中存在的问题、如何解决存在的问题 MDC介绍 简介: MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 、logback及log4j2 提供的一种方便在多线程条件下记录日志的功能。 MDC  可以看成是一个 与当前线程绑定的哈希表 ,可以往其

    2024年02月12日
    浏览(45)
  • SpringBoot的数据层解决方案

    🙈作者简介:练习时长两年半的Java up主 🙉个人主页:程序员老茶 🙊 ps:点赞👍是免费的,却可以让写博客的作者开心好久好久😎 📚系列专栏:Java全栈,计算机系列(火速更新中) 💭 格言:种一棵树最好的时间是十年前,其次是现在 🏡动动小手,点个关注不迷路,感

    2024年01月16日
    浏览(45)
  • SpringBoot中文乱码问题解决方案

    在Spring Boot中,确实没有像传统Web应用程序中需要使用web.xml配置文件。对于中文乱码问题,你可以采取以下几种方式来解决: 在application.properties文件中添加以下配置: 这里将字符集设置为UTF-8,并启用了HTTP编码配置。 如果你使用的是Spring Boot 2.x版本,可以尝试在pom.xml文件

    2024年02月04日
    浏览(42)
  • SpringBoot 循环依赖的症状和解决方案

    循环依赖是指在Spring Boot 应用程序中,两个或多个类之间存在彼此依赖的情况,形成一个循环依赖链。 在这种情况下,当一个类在初始化时需要另一个类的实例,而另一个类又需要第一个类的实例时,就会出现循环依赖问题。这会导致应用程序无法正确地初始化和运行,因为

    2024年02月09日
    浏览(42)
  • SpringBoot+JWT实现单点登录解决方案

    一、什么是单点登录? 单点登录是一种统一认证和授权机制,指在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的系统,不需要重新登录验证。 单点登录一般用于互相授信的系统,实现单一位置登录,其他信任的应用直接免登录的方式,在多个应用系统中,

    2024年02月12日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包