• SpringBoot 中异步多线程的MDC日志跟踪


    前言:

            SpringBoot 如果不涉及异步多线程日志跟踪相对简单,可以参考logback + MDC 搭建 springboot 的日志系统,如果涉及异步多线程就需要重写线程池,线程池有很多方法,其实没必要都重写,只要把提交线程的方法重写即可。

    一、MDC 日志跟踪的核心方法

            先讲一下 SpringBoot 请求的流转:请求到来先走的是 Filter(过滤器),然后走 Interceptor(拦截器),再走 Controller (路由器),再转到 Service(服务层),最后到持久层。日志跟踪的方式就是在 Filer 或 Interceptor 中填入 MDC 信息,这样根据 requestId 就可以串起来整个请求流程。

    MDC 填充 requestId 方法 如下:

    1. public class RequestIdUtil {
    2. public static final String REQUEST_ID = "requestId";
    3. public static void setRequestId() {
    4. MDC.put(REQUEST_ID, UUID.randomUUID().toString());
    5. }
    6. public static String getRequestId() {
    7. return MDC.get(REQUEST_ID);
    8. }
    9. public static void clear() {
    10. MDC.clear();
    11. }
    12. }

    Filter 中调用上述方法填充 MDC 方式如下:

    1. @Slf4j
    2. @Component
    3. public class RequestIdFilter implements Filter {
    4. @Override
    5. public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    6. RequestIdUtil.setRequestId();
    7. log.info("entry filter........");
    8. try {
    9. filterChain.doFilter(servletRequest, servletResponse);
    10. } finally {
    11. log.info("主线程清理 mdc...");
    12. RequestIdUtil.clear();
    13. }
    14. }
    15. }

    二、重写线程池方法

            异步多线程的使用可以参考 SpringBoot开启异步多线程。当请求到来时,主线程在 Filter 中设置了 MDC 信息,但是主线程开启子线程时子线程的MDC跟主线程脱钩,解决方案核心就是开启子线程时复制一份主线程的MDC信息给子线程,所以需要重写线程池的方法(具体就是在提交线程的方法中复制MDC)。

            另外,线程池的方法很多,都重写肯定是没问题的,但是有违程序猿的简约美,推荐一个方法:把所有 ThreadPoolTaskExecutor 的方法都 Override,再在每个函数第一行打上日志,然后触发异步线程池就可以看到哪些线程池函数被调用了,再重写这些被调用的方法肯定没错:

    1. @Slf4j
    2. public class MdcThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
    3. /**
    4. * 接口请求开启的异步线程会调用下述方法
    5. */
    6. @Override
    7. public void execute(Runnable task) {
    8. log.info("接口触发的异步多线程...");
    9. Map context = MDC.getCopyOfContextMap(); //复制主线程MDC
    10. super.execute(() -> {
    11. if (null != context) {
    12. MDC.setContextMap(context); //主线程MDC赋予子线程
    13. } else {
    14. RequestIdUtil.setRequestId(); //主线程没有MDC就自己生成一个
    15. }
    16. try {
    17. task.run();
    18. } finally {
    19. try {
    20. RequestIdUtil.clear();
    21. } catch (Exception e) {
    22. log.warn("MDC clear exception:{}", e.getMessage());
    23. }
    24. }
    25. });
    26. }
    27. /**
    28. * 定时任务会调用下述方法
    29. */
    30. @Override
    31. public Future submit(Callable task) {
    32. log.info("定时触发的异步多线程...");
    33. Map context = MDC.getCopyOfContextMap();
    34. return super.submit(() -> {
    35. if (null != context) {
    36. MDC.setContextMap(context); //主线程MDC赋予子线程
    37. } else {
    38. RequestIdUtil.setRequestId(); //主线程没有MDC就自己生成一个
    39. }
    40. try {
    41. return task.call();
    42. } finally {
    43. try {
    44. RequestIdUtil.clear();
    45. } catch (Exception e) {
    46. log.warn("MDC clear exception:{}", e.getMessage());
    47. }
    48. }
    49. });
    50. }
    51. }

    线程池实现的时候就不再用 ThreadPoolTaskExecutor 而是上述 MdcThreadPoolTaskExecutor:

    1. @Slf4j
    2. @Configuration
    3. @EnableAsync
    4. public class ThreadPoolConfig {
    5. @Bean("normalThreadPool")
    6. public Executor executorNormal() {
    7. MdcThreadPoolTaskExecutor executor = new MdcThreadPoolTaskExecutor();
    8. executor.setCorePoolSize(1);
    9. executor.setMaxPoolSize(2);
    10. executor.setQueueCapacity(1);
    11. executor.setKeepAliveSeconds(60);
    12. executor.setThreadNamePrefix("NORMAL--");
    13. executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
    14. executor.initialize();
    15. return executor;
    16. }
    17. @Bean("scheduleThreadPool")
    18. public Executor executorSchedule() {
    19. MdcThreadPoolTaskExecutor executor = new MdcThreadPoolTaskExecutor();
    20. executor.setCorePoolSize(5);
    21. executor.setMaxPoolSize(8);
    22. executor.setQueueCapacity(2);
    23. executor.setKeepAliveSeconds(60);
    24. executor.setThreadNamePrefix("SCHEDULE--");
    25. executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
    26. executor.initialize();
    27. return executor;
    28. }
    29. }

    三、测试

    1.测试接口调用的异步

    写一个普通异步方法类:

    1. @Slf4j
    2. @Service
    3. public class ThreadTaskService {
    4. public static int threadNum = 0;
    5. @Async("normalThreadPool")
    6. public CompletableFuture task(int index) {
    7. String result = "asy start...";
    8. log.info("task {} start...", index);
    9. try {
    10. Thread.sleep(10000);
    11. result = "asy end...";
    12. } catch (InterruptedException e) {
    13. e.printStackTrace();
    14. }
    15. log.info("task {} end...", index);
    16. return CompletableFuture.completedFuture(result);
    17. }
    18. }

    提供一个调用入口:

    1. @Slf4j
    2. @RestController
    3. @RequestMapping("/thread")
    4. public class ThreadTaskController {
    5. @Autowired
    6. ThreadTaskService taskService;
    7. @GetMapping(value = "/start")
    8. public String getValue() {
    9. threadNum++;
    10. CompletableFuture result = taskService.task(threadNum);
    11. return "hello...";
    12. }
    13. }

    触发上述接口可看到日志:

    子线程完美的复制集成了主线程的 requestId,并且可以看到是调用线程池的 execute(Runnable task) 方法。

    2.测试定时调用的异步

    准备一个定时类:

    1. @Slf4j
    2. @Configuration
    3. @EnableScheduling
    4. public class ScheduleTask {
    5. private static int count = 0;
    6. private ThreadLocal num = new ThreadLocal<>();
    7. @Async("scheduleThreadPool")
    8. @Scheduled(initialDelay = 3000, fixedDelay = 1000 * 5)
    9. public void task() {
    10. count++;
    11. num.set(count);
    12. log.info("内部定时线程 {} 开启...", num.get());
    13. getTask();
    14. }
    15. private void getTask() {
    16. try {
    17. Thread.sleep(6000);
    18. } catch (InterruptedException e) {
    19. e.printStackTrace();
    20. }
    21. log.info("内部定时线程 {} 结束", num.get());
    22. }
    23. }

    触发上述定时可看到日志:

    定时任务的线程启动时因为没有主线程,所以打印的日志 requestId 为空,重新设置 requestId 后可以在后续的请求中串起来。 

    四、后续

    还有一些其他请求的跟踪,例如 feign、http 等,整体的解决方案就是在发起请求前塞一个请求id进去,然后返回时再拿出来;如有需求可自己研究。

  • 相关阅读:
    RTC 性能自动化工具在内存优化场景下的实践
    MLX90640 红外热成像仪测温传感器 手机 APP 软件 RedEye 连接详细
    基于51单片机智能家居家电继电器开关插座定时WiFi无线proteus仿真原理图PCB
    在不损失质量的情况下减小PDF 文件大小的 6 种方法
    解决docker Error response from daemon故障
    uniapp使用阿里图标显示查找文件失败,在H5端图标显示正常但是在移动端不显示图标的问题解决,uniapp中如何使用阿里巴巴图标库
    CentOS Linux下CMake二进制文件安装并使用Visual Studio调试
    目标检测YOLO实战应用案例100讲-基于小样本学习和空间约束的濒危动物目标检测
    贪心法求解问题
    AWS的RDS数据库开启慢查询日志
  • 原文地址:https://blog.csdn.net/qingquanyingyue/article/details/126682385