用ThreadLocal做链路追踪(演变升级版)

news2024/12/23 12:42:39

前言
1、ThreadLocal是线程变量,线程之间彼此隔离,天生线程安全。因为它是跟着线程走的,考虑到这点,它很适合做链路追踪(TraceId)
2、当我们写的接口接收到其它地方(可能是前端、也可能是其它服务)发来的请求时,此刻,我们的接口所在的服务称作服务端【Server】,而请求方称作客户端【Client】;当我们的接口中再请求其他服务,此刻,我们的接口所在的服务称作客户端【Client】,而被请求方称作客户端【Client】

线程变量承载体

public class TraceIdHolder {

    // 初版, 普通的 ThreadLocal, 只适用于不开辟子线程的情况
    //private static final ThreadLocal<String> TRACE_ID_HOLDER = new ThreadLocal<>();

    // 升级版,new子线程时,为了让线程变量继承,得改用 InheritableThreadLocal
    //private static final ThreadLocal<String> TRACE_ID_HOLDER = new InheritableThreadLocal<>();

    // 最终版,线程池中开辟线程时,存在线程复用(上个请求使用的线程的线程变量会残留),得改用 TransmittableThreadLocal + TtlExecutors
    private static final ThreadLocal<String> TRACE_ID_HOLDER = new TransmittableThreadLocal<>();

    public TraceIdHolder() {
    }

    public static void set(String traceId) {
        TRACE_ID_HOLDER.set(traceId);
    }

    public static String get() {
        return TRACE_ID_HOLDER.get();
    }

    public static String remove() {
        String traceId = TRACE_ID_HOLDER.get();
        TRACE_ID_HOLDER.remove();
        return traceId;
    }
}

打印日志
服务端的日志打印很好做,用过滤器Filter即可,每次请求打过来,记录下请求路径、请求头、参数

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.cqf.config.constant.Constant;
import com.cqf.config.wrapper.RequestWrapper;
import com.cqf.threadLocal.TraceIdHolder;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
import java.util.UUID;

@Order(1)
@WebFilter(filterName = "logFilter", urlPatterns = "/*")
public class LogFilter implements Filter {

    private static final Logger LOGGER = LoggerFactory.getLogger(LogFilter.class);
    private static final String GET = "GET";
    private static final String POST = "POST";
    private static final String PUT = "PUT";
    private static final String DELETE = "DELETE";

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String url = request.getRequestURL().toString();
        String method = request.getMethod();
        Enumeration<String> headerNames = request.getHeaderNames();
        String next;
        StringBuilder bodyBuffer = new StringBuilder();
        boolean hasTraceId = false;
        while ((next = headerNames.nextElement()) != null) {
            // 过滤掉postman自带的请求头
            if (Constant.USELESS_HEADER.contains(next.toLowerCase())) {
                continue;
            }
            String headerValue = request.getHeader(next);
            // 每次外部请求过来,在这里设置trace-id
            if ("trace-id".equalsIgnoreCase(next) && StringUtils.isNotBlank(headerValue)) {
                hasTraceId = true;
                TraceIdHolder.set(headerValue);
            }
            bodyBuffer.append(next).append("=").append(headerValue).append(", ");
        }
        if (!hasTraceId) {
            String traceId = UUID.randomUUID().toString().replace("-", "");
            bodyBuffer.append("trace-id").append("=").append(traceId).append(", ");
            TraceIdHolder.set(traceId);
        }
        if (StringUtils.isNotBlank(bodyBuffer.toString())) {
            bodyBuffer.delete(bodyBuffer.length() - 2, bodyBuffer.length());
        }
        RequestWrapper requestWrapper = new RequestWrapper(request);
        String bodyString = requestWrapper.getBodyString();
        String payload = JSON.toJSONString(JSONObject.parseObject(bodyString),
                SerializerFeature.PrettyFormat,
                SerializerFeature.WriteMapNullValue,
                SerializerFeature.WriteDateUseDateFormat);

        // before request
        String msg = "[Server] Before request [" +
                method +
                " uri=" +
                url +
                "; " +
                "headers={" +
                bodyBuffer.toString() +
                "}]";
        if (!GET.equalsIgnoreCase(method)) {
            msg = msg + "; payload=" + payload + "]";
        }
        LOGGER.info(msg);
        // 在执行链调用前就要把trace-id放在响应头里
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        response.setHeader("trace-id", TraceIdHolder.get());
        filterChain.doFilter(requestWrapper, servletResponse);
        int status = response.getStatus();

        // after request
        LOGGER.info("[Server] After request, status=" +
                status +
                " [" +
                method +
                " uri=" +
                url +
                "; " +
                "headers={" +
                bodyBuffer.toString() +
                "}]");
    }
}

作为客户端时,自己是主动发起方,所以要给RestTemplate设置拦截器,发送请求前打印日志

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.cqf.config.constant.Constant;
import com.cqf.threadLocal.TraceIdHolder;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

@Configuration
public class WebConfig {

    private static final Logger LOGGER = LoggerFactory.getLogger(WebConfig.class);
    private final String GET = "GET";
    private final String POST = "POST";
    private final String PUT = "PUT";
    private final String DELETE = "DELETE";

    private ClientHttpRequestInterceptor logClientHttpRequestInterceptor() {
        return ((httpRequest, body, clientHttpRequestExecution) -> {
            // 设置公共请求头
            httpRequest.getHeaders().set("trace-id", TraceIdHolder.get());

            String methodValue = httpRequest.getMethodValue();
            HttpHeaders headers = httpRequest.getHeaders();
            String payload = new String(body, 0, body.length, StandardCharsets.UTF_8.name());
            StringBuffer buffer = new StringBuffer("");
            headers.forEach((key, value) -> {
                if (!Constant.USELESS_HEADER.contains(key.toLowerCase())) {
                    buffer.append(key).append("=").append(value.get(0)).append(", ");
                }
            });
            if (StringUtils.isNotBlank(buffer.toString())) {
                buffer.delete(buffer.length() - 2, buffer.length());
            }

            // 格式化json字符串,方便再日志中查看
            payload = JSON.toJSONString(JSONObject.parseObject(payload),
                    SerializerFeature.PrettyFormat,
                    SerializerFeature.WriteMapNullValue,
                    SerializerFeature.WriteDateUseDateFormat);

            // before request
            String msg = "[Client] Before request [" +
                    methodValue +
                    " uri=" +
                    httpRequest.getURI() +
                    "; " +
                    "headers={" +
                    buffer.toString() + "}";

            if (!GET.equalsIgnoreCase(methodValue)) {
                msg = msg + "; payload=" + payload;
            }
            msg += "]";
            LOGGER.info(msg);
            ClientHttpResponse response = clientHttpRequestExecution.execute(httpRequest, body);

            // after request
            LOGGER.info("[Client] After request, status=" +
                    response.getStatusCode().value() +
                    " [" +
                    methodValue +
                    " uri=" +
                    httpRequest.getURI() +
                    "; " +
                    "headers={" +
                    buffer.toString() +
                    "}]");
            return response;
        });
    }

    @Bean
    public RestTemplate restTemplate() {
        SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
        requestFactory.setConnectTimeout(5000);
        requestFactory.setReadTimeout(5000);
        RestTemplate restTemplate = new RestTemplate(requestFactory);
        // 给RestTemplate 设置拦截器,发送请求前打印日志
        ClientHttpRequestInterceptor interceptor = logClientHttpRequestInterceptor();
        List<ClientHttpRequestInterceptor> interceptorList = new ArrayList<>();
        interceptorList.add(interceptor);
        restTemplate.setInterceptors(interceptorList);
        return restTemplate;
    }
}

Controller的编写

    @Resource
    private RestTemplate restTemplate;

    // 最终版使用,配合TransmittableThreadLocal一起用才生效。线程池需要包装一下。线程数量设置为1是为了发起第二次请求时就能暴露出线程复用带来的问题
    private static final ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));

    @GetMapping("/method1")
    public String method1() {
        // 初版, 普通的 ThreadLocal, 只适用于不开辟子线程的情况
        //restTemplate.exchange("http://localhost:5555/test02", HttpMethod.GET, new HttpEntity<>(null, null), String.class);

        // 升级版,new子线程时,为了让线程变量继承,得改用 InheritableThreadLocal
        //new Thread(() -> {
        //    restTemplate.exchange("http://localhost:5555/test02", HttpMethod.GET, new HttpEntity<>(null, null), String.class);
        //}).start();

        // 最终版,线程池中开辟线程时,存在线程复用(上个请求使用的线程的线程变量会残留),得改用 TransmittableThreadLocal + TtlExecutors
        CompletableFuture.runAsync(() -> {
            restTemplate.exchange("http://localhost:5555/test02", HttpMethod.GET, new HttpEntity<>(null, null), String.class);
        }, executorService);
        try { future.get(); } catch (Exception e) { e.printStackTrace(); }
        return "1";
    }

最终版,postMan连续发送两次请求的日志
在这里插入图片描述
总结
初版(ThreadLocal)的缺点:当new新线程时,子线程获取不到父线程的变量,导致trace-id丢失。
于是出现了升级版(InheritableThreadLocal):解决了初版的问题,但是当线程池中开辟线程时,线程复用会残留上一次的trace-id,导致混乱不准。
于是出现了最终版(TransmittableThreadLocal + TtlExecutors)

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

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

相关文章

python如何开发一个电商进销存管理系统?

让我们来看一下题主的需求&#xff1a; 管理公司的淘宝天猫平台&#xff0c;后端仓库&#xff0c;采购进行数据同步。其中最主要的还是要对接淘宝API &#xff0c;实现实时订单的通知&#xff0c;同步淘宝订单&#xff0c;管理买家信息&#xff0c;发货&#xff0c;财务统计等…

亚马逊举报差评有什么作用?有没有可以举报差评的软件?

在亚马逊上举报差评具有以下作用&#xff1a; 1、维护信誉和公平性&#xff1a; 举报差评有助于维护亚马逊市场的信誉和公平性。虚假或不当的差评可能会误导其他消费者&#xff0c;影响他们做出购买决策。通过举报这些问题&#xff0c;可以确保评价体现真实的用户体验&#xf…

【SpringCloud技术专题】「Resilience4j入门指南」(1)轻量级熔断框架的入门指南

基础介绍 Resilience4j是一款轻量级&#xff0c;易于使用的容错库&#xff0c;其灵感来自于Netflix Hystrix&#xff0c;但是专为Java 8和函数式编程而设计。轻量级&#xff0c;因为库只使用了Vavr&#xff0c;它没有任何其他外部依赖下。相比之下&#xff0c;Netflix Hystrix…

Beats:使用 Filebeat 将 golang 应用程序记录到 Elasticsearch - 8.x

毫无疑问&#xff0c;日志记录是任何应用程序最重要的方面之一。 当事情出错时&#xff08;而且确实会出错&#xff09;&#xff0c;我们需要知道发生了什么。 为了实现这一目标&#xff0c;我们可以设置 Filebeat 从我们的 golang 应用程序收集日志&#xff0c;然后将它们发送…

大数据培训前景怎么样?企业需求量大吗

大数据行业对大家来说并不陌生&#xff0c;大数据行业市场人才需求量大&#xff0c;越早入行越有优势&#xff0c;发展机会和上升空间等大。不少人通过大数据培训来提升自己的经验和自身技术能力&#xff0c;以此来获得更好的就业机会。 2023大数据培训就业前景怎么样呢?企业需…

落地大模型应知必会(3): 如何构建多任务的LLM应用

编者按&#xff1a;今年以来&#xff0c;大语言模型(LLM)已被广泛应用于各种自然语言处理任务&#xff0c;也越来越多地被用于构建复杂的语言应用。但是构建多任务的 LLM 应用仍面临一定的挑战&#xff0c;需要解决任务组合和调控等问题。 本文内容介绍了构建多任务 LLM 应用可…

win10中Docker安装、构建镜像、创建容器、Vscode连接实例

Docker方便一键构建项目所需的运行环境&#xff1a;首先构建镜像(Image)。然后镜像实例化成为容器(Container)&#xff0c;构成项目的运行环境。最后Vscode连接容器&#xff0c;方便我们在本地进行开发。下面以一个简单的例子介绍在win10中实现&#xff1a;Docker安装、构建镜像…

STM32--TIM定时器(2)

文章目录 输出比较PWM输出比较通道参数计算舵机简介直流电机简介TB6612 PWM基本结构PWM驱动呼吸灯PWM驱动舵机PWM控制电机 输出比较 输出比较&#xff0c;简称OC&#xff08;Output Compare&#xff09;。 输出比较的原理是&#xff0c;当定时器计数值与比较值相等或者满足某种…

【计算机视觉|生成对抗】改进的生成对抗网络(GANs)训练技术

本系列博文为深度学习/计算机视觉论文笔记&#xff0c;转载请注明出处 标题&#xff1a;Improved Techniques for Training GANs 链接&#xff1a;[1606.03498v1] Improved Techniques for Training GANs (arxiv.org) 摘要 本文介绍了一系列应用于生成对抗网络&#xff08;G…

2023国自然预计本周公布!如何第一时间知道是否中标?

国自然公布时间越来越近了&#xff0c;大家普遍关心今年国自然的具体公布时间。 基金委官网上明确&#xff1a;国家自然科学基金项目从接收至审批完成一般需要5个月左右时间。 集中接收的大部分项目类型&#xff0c;资助结果一般在当年8月中下旬公布&#xff0c;其余项目根据…

时序预测 | MATLAB实现基于GRU门控循环单元的时间序列预测-递归预测未来(多指标评价)

时序预测 | MATLAB实现基于GRU门控循环单元的时间序列预测-递归预测未来(多指标评价) 目录 时序预测 | MATLAB实现基于GRU门控循环单元的时间序列预测-递归预测未来(多指标评价)预测结果基本介绍程序设计参考资料 预测结果 基本介绍 1.Matlab实现GRU门控循环单元时间序列预测未…

vue下载模板

<div class"el-upload__tip"><el-buttonstyle"margin-top: 2px"size"small"type"primary"click"downloadTemplate()">下载模板</el-button></div>/***todo 下载模板*/downloadTemplate() {getDownl…

WebStorm修改默认打开的浏览器

有两种方式第一种修改系统默认浏览器 我采用的是下面这种&#xff0c;在webstorm中修改 将浏览器设置为默认的浏览器即可

关于Java中synchronized的实现原理

并发编程的三个理念 原子性&#xff1a;一个操作要么全部完成&#xff0c;要么全部失败。可见性&#xff1a;当一个线程对共享变量进行修改后&#xff0c;其他线程也应立刻看到。有序性&#xff1a;程序按照顺序执行 synchronized基本使用 修饰静态方法&#xff0c;锁的是类…

mysql 笔记(二)-mysql存储引擎

存储引擎在mysql体系架构中位于第三层, 负责mysql中的数据存储和提取,根据mysql提供的文件访问层抽象接口定制的一种文件访问机制. 使用show engines命令可以查看当前数据库支持的引擎信息. 从截图可看到, mysql 默认的存储引擎是InnoDB,支持事务,行锁,外键,支持分布式事务(…

SSD是否可以提升游戏性能或帧数?

​在购买这种新型硬盘之前&#xff0c;你可能会有些疑问。在这篇文章中&#xff0c;我将解释什么是固态硬盘&#xff08;SSD&#xff09;&#xff0c;它是否能提升游戏性能&#xff0c;以及如何将你的旧硬盘替换为新的固态硬盘。​ 更换SSD可以让我的游戏运行更流畅吗&#xff…

TB/TM-商品详情

一、接口参数说明&#xff1a; item_get-获得商品详情&#xff0c;点击更多API调试&#xff0c;请移步注册API账号点击获取测试key和secret 公共参数 请求地址: https://api-gw.onebound.cn/taobao/item_get 名称类型必须描述keyString是调用key&#xff08;点击获取测试key…

ssm+vue医院住院管理系统源码和论文PPT

ssmvue医院住院管理系统源码和论文PPT012 开发工具&#xff1a;idea 数据库mysql5.7(mysql5.7最佳) 数据库链接工具&#xff1a;navcat,小海豚等 开发技术&#xff1a;java ssm tomcat8.5 摘 要 随着时代的发展&#xff0c;医疗设备愈来愈完善&#xff0c;医院也变成人们生…

解密Flink的状态管理:探索流处理框架的数据保留之道,释放流处理的无限潜能!

水善利万物而不争&#xff0c;处众人之所恶&#xff0c;故几于道&#x1f4a6; 文章目录 一、什么是状态二、应用场景三、Flink中状态的分类四、算子状态1. 列表状态&#xff08;List State&#xff09;2. 广播状态&#xff08;Broadcast State&#xff09; 五、键控状态1. Val…

基于Springboot+vue+elementUI+MySQL的学生信息管理系统(一)前端部分

源码在本人博客资源当中&#xff0c;本文为项目代码的详细介绍解释&#xff0c;供于大家学习使用 Vue项目的入口文件&#xff1a;mian.js //vue项目入口文件 //导入vue import Vue from vue //导入根组件app import App from ./App //导入路由文件 import router from ./rout…