分布式微服务架构日志调用链路跟踪-traceId

分布式微服务架构日志调用链路跟踪-traceId

在ELK日志集成平台里(日志的写入,收集,跟踪,搜索,分析)

背景知识

在xxx(博主之前的公司),每个前端请求里面,都会在request的header区携带一个traceId 随机数值,用来跟踪在后端的调用链路栈打印.通过ES收集的日志数据,在ELK日志集成平台里,用traceId就能得到用户请求的完整链路,排查问题的效率非常高.那么具体是怎么设计的?

同样的,每个请求的用户会话信息该怎么传递?

traceId,UserId,facilityId,tenantId.

遇到的场景

有几个难题需要思考: 1.客户端调用方法中,遇到了rpc该怎么传递traceId? 2.遇到了feign接口该怎么传递traceId? 3.遇到了mq该怎么传递tranceId? 4.遇到了线程池里的新线程该怎么传递tranceId? 下面针对各个场景进行逐一的分析

xxx的实现方案:

在gateway中对sid进行校验和初步存储 gateway-GlobalFilter-SIDCheckedFilter.java gateway的原理:一系列的router和filter集合.

常见:

  • IP白名单和黑名单控制
  • SID检测
  • 请求签名检测,防止恶意构建请求
  • 用户会话检测,防止非系统用户请求
  private static String getSID(ServerHttpRequest request) {
      String sid = request.getHeaders().getFirst(Constants.HEADER_SID);
      // 将sid放到日志的变量中
      MDC.put(Constants.HEADER_SID,sid);
      return sid;
  }
  @Override
  public Mono<Void> execute(ServerHttpRequest request, ServerWebExchange exchange, GatewayFilterChain chain) {
      String sid = getSID(request);
      if(logger.isInfoEnabled()) {
          String url = StringUtils.defaultIfBlank(request.getPath().value(), "");
          String ip = IpUtils.getIp(request);
          MDC.put(MDC_SID,sid);
          MDC.put(MDC_URI,url);
          MDC.put(MDC_IP,ip);
          MDC.put(MDC_USER,null);
          logger.info("网关请求..");
      }
      if (validSid) {
          if (StringUtils.isBlank(sid)) {
              logger.info("{} SID为空", this.getRequestUri(request));
              // 构造返回错误信息
              ApiResponse<String> responseMap = ApiResponse.failResp(RequestCode.SID_ISNULL);
              return returnError(exchange, HttpStatus.BAD_REQUEST, responseMap);
          }
          // 解决高并发状态的多次重复请求问题
          String redisSidKey = GatewayRedisKeyConstants.GATEWAY_SID_KEY_PREFIX + sid;
          boolean isSuccess = redisTemplate.opsForValue().setIfAbsent(redisSidKey, GATEWAY_SID_KEY_DEF_VAL, GATEWAY_SID_KEY_EXPIRE_TIME, TimeUnit.MINUTES);
          if (!isSuccess) {
              logger.info("SID已存在,重复请求:{}", sid);
              ApiResponse<String> responseMap = ApiResponse.failResp(RequestCode.SID_REPEATED_REQUESTS);
              return returnError(exchange, HttpStatus.BAD_REQUEST, responseMap);
          }
      }
      exchange = afterValidHandler(exchange);//验证过后处理器
      return chain.filter(exchange);
  }


  /**
   * 验证过后处理器
   *
   * @param exchange
   * @return
   */
  @Override
  public ServerWebExchange afterValidHandler(ServerWebExchange exchange) {
      ServerHttpRequest request = exchange.getRequest();
      String sid = request.getHeaders().getFirst(Constants.HEADER_SID);
      //不要验证SID的情况下,如果SI为空,默认生成一个SID,供下游系统使用
      if (StringUtils.isBlank(sid)) {
          //生成sid
          String gatewaySid = "gateway_" + DateTimeUtils.format(LocalDateTime.now(), DateTimeUtils.FORMAT_1) + RandomUtil.randomNumbers(6);
          //构造新的ServerHttpRequest
          ServerHttpRequest.Builder builder = request.mutate()
                  //往header中添加网关生成的SID
                  .header(com.ess.framework.commons.constant.Constants.HEADER_SID, gatewaySid);
          // 将sid放到日志的变量中
          MDC.put(Constants.HEADER_SID,gatewaySid);
          exchange = exchange.mutate().request(builder.build()).build();
      }
      return exchange;
  }

定义个framework-boot的依赖包

这个依赖包内使用interceptor拦截器对web请求做拦截,拿到traceId,通过threadLocal对象放入一个Context对象中.每个请求对应一个ThreadLocal对象. 每个服务的全局拦截器

  @RefreshScope
  public abstract class BootWebConfigurer implements WebMvcConfigurer {
      protected final Logger logger = LoggerFactory.getLogger(this.getClass());

      @Autowired
      private RequestMappingHandlerAdapter requestMappingHandlerAdapter;

      @Bean
      @RefreshScope
      public GatewayInterceptor getGatewayInterceptor() {
          return new GatewayInterceptor();
      }

      @Bean
      @RefreshScope
      public SIDInterceptor getSIDInterceptor() {
          return new SIDInterceptor();
      }

      @Bean
      @RefreshScope
      public InnerInterceptor getInnerInterceptor() {
          return new InnerInterceptor();
      }

      @Bean
      public TokenInterceptor getTokenInterceptor() {
          return new TokenInterceptor();
      }

      @Bean
      public LogMdcInterceptor getLogMdcInterceptor() {
          return new LogMdcInterceptor();
      }

      @Bean
      public UnionInterceptor getUnionInterceptor() {
          return new UnionInterceptor();
      }

      @Override
      public void addInterceptors(InterceptorRegistry registry) {
          registry.addInterceptor(getSIDInterceptor()).addPathPatterns("/**").excludePathPatterns(ResouceConstants.EXCLUDE_STATIC_RESOURCE);
          registry.addInterceptor(getGatewayInterceptor()).addPathPatterns("/**").excludePathPatterns(ResouceConstants.EXCLUDE_STATIC_RESOURCE);
          registry.addInterceptor(getInnerInterceptor()).addPathPatterns("/**").excludePathPatterns(ResouceConstants.EXCLUDE_STATIC_RESOURCE);
          registry.addInterceptor(getTokenInterceptor()).addPathPatterns("/**").excludePathPatterns(ResouceConstants.EXCLUDE_STATIC_RESOURCE);
          registry.addInterceptor(getLogMdcInterceptor()).addPathPatterns("/**").excludePathPatterns(ResouceConstants.EXCLUDE_STATIC_RESOURCE);
          // 添加联盟拦截器
          registry.addInterceptor(getUnionInterceptor()).addPathPatterns("/**").excludePathPatterns(ResouceConstants.EXCLUDE_STATIC_RESOURCE);
      }

      /**
       * 初始化SIDReturnValueHandler  对所有的响应返回SID字段
       */
      @PostConstruct
      public void initSidHandler() {
          final List<HandlerMethodReturnValueHandler> originalHandlers = new ArrayList<>(requestMappingHandlerAdapter.getReturnValueHandlers());
          final int deferredPos = obtainValueHandlerPosition(originalHandlers, DeferredResultMethodReturnValueHandler.class);
          SIDReturnValueHandler decorator = null;
          for (HandlerMethodReturnValueHandler handler : originalHandlers) {
              if (handler instanceof RequestResponseBodyMethodProcessor) {
                  decorator = new SIDReturnValueHandler((RequestResponseBodyMethodProcessor) handler);
                  break;
              }
          }
          originalHandlers.add(deferredPos + 1, decorator);
          requestMappingHandlerAdapter.setReturnValueHandlers(originalHandlers);
      }

      private int obtainValueHandlerPosition(final List<HandlerMethodReturnValueHandler> originalHandlers, Class<?> handlerClass) {
          for (int i = 0; i < originalHandlers.size(); i++) {
              final HandlerMethodReturnValueHandler valueHandler = originalHandlers.get(i);
              if (handlerClass.isAssignableFrom(valueHandler.getClass())) {
                  return i;
              }
          }
          return -1;
      }

      @Override
      public void addResourceHandlers(ResourceHandlerRegistry registry) {
          registry.addResourceHandler("/");
          registry.addResourceHandler("/favicon.ico").addResourceLocations("classpath:/favicon.ico");
          registry.addResourceHandler("app.html").addResourceLocations("classpath:/");
          registry.addResourceHandler("/error");
          // Swagger2配置
          registry.addResourceHandler("/v2/**");
          registry.addResourceHandler("/swagger-resources/**");
          registry.addResourceHandler("/csrf");
          registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
          registry.addResourceHandler("/swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
          registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
      }

      /**
       * 自定义扩展消息转换器
       *
       * @param converters
       */
      @Override
      public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
          //定义fastjson转换消息对象
          FastJsonHttpMessageConverter fastJsonConverter = new FastJsonHttpMessageConverter();
          //添加fastjson全局配置
          FastJsonConfig fastJsonConfig = new FastJsonConfig();
          fastJsonConfig.setSerializerFeatures(
                  // 排序配置
                  SerializerFeature.SortField,
                  SerializerFeature.MapSortField,
                  // 避免对象重复引用
                  SerializerFeature.DisableCircularReferenceDetect,
                  // 格式化输出
                  SerializerFeature.PrettyFormat
          );
          fastJsonConfig.setCharset(Charset.forName("UTF-8"));
          fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
          //默认json会对属性里的json字符串值进行排序,加了这个Feature.OrderedField则会禁止排序
          fastJsonConfig.setFeatures(Feature.OrderedField);
          fastJsonConverter.setFastJsonConfig(fastJsonConfig);
          //添加支持的MediaType类型
          fastJsonConverter.setSupportedMediaTypes(Lists.newArrayList(MediaType.APPLICATION_JSON));
          converters.set(0, fastJsonConverter);

          //BigDecimal格式化
          SerializeConfig serializeConfig = SerializeConfig.getGlobalInstance();
          serializeConfig.put(BigDecimal.class, BigDecimalConfigure.instance);
          fastJsonConfig.setSerializeConfig(serializeConfig);

          //字节数组消息转换器(供文件下载使用)
          converters.add(new ByteArrayHttpMessageConverter());

          Map<String, HttpMessageConverter<?>> convertersMap = new LinkedHashMap<>();
          //转换器去重,并设置所有转换器的默认编码
          for (HttpMessageConverter converter : converters) {
              String name = converter.getClass().getSimpleName();
              if (converter instanceof StringHttpMessageConverter) {
                  //设置StringHttpMessageConverter的默认编码为:UTF-8
                  ((StringHttpMessageConverter) converter).setDefaultCharset(Charset.forName("UTF-8"));
              }
              if (!convertersMap.containsKey(name)) {
                  convertersMap.put(name, converter);
              }
          }
          converters.clear();
          converters.addAll(Lists.newArrayList(convertersMap.values()));
      }


      @Bean
      public Converter<String, LocalDate> localDateConverter() {
          return new Converter<String, LocalDate>() {
              @Override
              public LocalDate convert(String source) {
                  return LocalDate.parse(source, Constants.DATE_FORMATTER);
              }
          };
      }

      @Bean
      public Converter<String, LocalDateTime> localDateTimeConverter() {
          return new Converter<String, LocalDateTime>() {
              @Override
              public LocalDateTime convert(String source) {
                  return LocalDateTime.parse(source, Constants.DATE_TIME_FORMATTER);
              }
          };
      }

      /**
       * Java常用时间类型序列化和反序列化
       * LocalDateTime、LocalDate、LocalTime、java.util.Date、java.sql.Date、Calendar、Timestamp
       * 解决通过Feign调用时,客户端是以上类型会报错问题。
       */
      @Bean
      public ObjectMapper objectMapper() {
          ObjectMapper objectMapper = new ObjectMapper();
          JavaTimeModule javaTimeModule = new JavaTimeModule();
          //定义序列化日期时间类型转换器
          javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeUtils.DEFAULT_FORMATTER));
          javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeUtils.YEAR_MONTH_DAY_FORMATTER));
          javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeUtils.HOUR_MINUTE_SECOND_FORMATTER));
          javaTimeModule.addSerializer(Date.class, new DateSerializer(true, new SimpleDateFormat(DateTimeUtils.DEFAULT_FORMAT)));
          javaTimeModule.addSerializer(java.sql.Date.class, new SqlDateSerializer().withFormat(true, new SimpleDateFormat(DateTimeUtils.DEFAULT_FORMAT)));
          javaTimeModule.addSerializer(Calendar.class, new CalendarSerializer(true, new SimpleDateFormat(DateTimeUtils.DEFAULT_FORMAT)));
          //定义反序列化日期时间类型转换器
          javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeUtils.DEFAULT_FORMATTER));
          javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeUtils.YEAR_MONTH_DAY_FORMATTER));
          javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeUtils.HOUR_MINUTE_SECOND_FORMATTER));
          javaTimeModule.addDeserializer(Date.class, new DateDeserializers.DateDeserializer(DateDeserializers.DateDeserializer.instance,
                  new SimpleDateFormat(DateTimeUtils.DEFAULT_FORMAT), DateTimeUtils.DEFAULT_FORMAT));
          javaTimeModule.addDeserializer(java.sql.Date.class, new DateDeserializers.SqlDateDeserializer(new DateDeserializers.SqlDateDeserializer(),
                  new SimpleDateFormat(DateTimeUtils.DEFAULT_FORMAT), DateTimeUtils.DEFAULT_FORMAT));
          javaTimeModule.addDeserializer(Calendar.class, new DateDeserializers.CalendarDeserializer(new DateDeserializers.CalendarDeserializer(),
                  new SimpleDateFormat(DateTimeUtils.DEFAULT_FORMAT), DateTimeUtils.DEFAULT_FORMAT));
          javaTimeModule.addDeserializer(Timestamp.class, new DateDeserializers.TimestampDeserializer(new DateDeserializers.TimestampDeserializer(),
                  new SimpleDateFormat(DateTimeUtils.DEFAULT_FORMAT), DateTimeUtils.DEFAULT_FORMAT));
          objectMapper.registerModule(javaTimeModule);

          objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

          return objectMapper;
      }
  }

sid拦截器

  package com.ess.framework.boot.interceptor;

  import com.ess.framework.commons.constant.Constants;
  import com.ess.framework.commons.response.RequestCode;
  import com.ess.framework.commons.utils.EssContextHolder;
  import com.ess.framework.commons.utils.ExceptionUtils;
  import org.apache.commons.lang3.StringUtils;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  import org.springframework.beans.factory.annotation.Value;
  import org.springframework.cloud.context.config.annotation.RefreshScope;
  import org.springframework.web.servlet.HandlerInterceptor;

  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletResponse;

  /**
   * SID 拦截器 ,验证SID是否为空
   */
  @RefreshScope
  public class SIDInterceptor implements HandlerInterceptor {
      protected final Logger logger = LoggerFactory.getLogger(this.getClass());

      /**
       * 是否验证SID
       */
      @Value("${valid.sid:false}")
      private boolean validSid = false;

      @Override
      public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
          String sid = request.getHeader(Constants.HEADER_SID);
          // 验证sid是否为空
          if (validSid && StringUtils.isBlank(sid)) {
              logger.warn("{} {}", request.getRequestURI(), RequestCode.SID_ISNULL.message());
              ExceptionUtils.throwBusiness(RequestCode.SID_ISNULL);
          }
          // 设置SID到线程变量 这是传输的关键
          if (StringUtils.isNotBlank(sid)) {
              EssContextHolder.setSID(sid);
          } else {
              EssContextHolder.setSID(null);
          }
          return true;
      }
  }

关于feign接口的传递tranceId

  package com.ess.framework.boot.interceptor;

  import com.ess.framework.commons.constant.Constants;
  import com.ess.framework.commons.utils.EssContextHolder;
  import com.ess.framework.commons.utils.IpUtils;
  import feign.RequestInterceptor;
  import feign.RequestTemplate;
  import lombok.extern.slf4j.Slf4j;
  import org.apache.commons.lang3.StringUtils;
  import org.slf4j.MDC;
  import org.springframework.web.context.request.RequestContextHolder;
  import org.springframework.web.context.request.ServletRequestAttributes;

  import javax.servlet.http.HttpServletRequest;

  /**
   * @ClassName FeignRequestInterceptor
   * @Description feign拦截器
   * @Date 2021/5/27 21:56
   * @Version
   */
  @Slf4j
  public class FeignRequestInterceptor implements RequestInterceptor {
      private final static String HEADER_ESS_FEIGN_IP = "ess-feign-ip";
      private final static String HEADER_IP = "ip";

      @Override
      public void apply(RequestTemplate requestTemplate) {
          try {
              String ip = null;
              String sid = null;
              String token = null;
              String unionId = null;
              /* ExecuteTaskUtils 线程池处理方案**/
              String threadName = Thread.currentThread().getName();
              if (StringUtils.startsWith(threadName, "ess-task-pool") || StringUtils.startsWith(threadName, "ess-async-pool")) {
                  sid = MDC.get(Constants.HEADER_SID);
                  ip = MDC.get(HEADER_IP);
                  unionId  = EssContextHolder.getUnionId();
                  token = EssContextHolder.getToken();
              } else {
                  ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                  if (attributes == null) {
                      return;
                  }
                  HttpServletRequest request = attributes.getRequest();
                  token = request.getHeader(Constants.HEADER_TOKEN);
                  sid = request.getHeader(Constants.HEADER_SID);
                  ip = IpUtils.getIPAddress(request);
                  unionId  = request.getHeader(Constants.HEADER_UNIONID);
              }
              requestTemplate.header(Constants.HEADER_TOKEN, token);
              requestTemplate.header(Constants.HEADER_SID, sid);
              requestTemplate.header(Constants.HEADER_UNIONID, unionId);
              requestTemplate.header(HEADER_ESS_FEIGN_IP, ip);
              // feign调用都设置网关标识设置为true,防止被拦截
              requestTemplate.header(Constants.PASS_GATEWAY, "true");
          } catch (Exception e) {
              log.error("拦截器异常", e);
          }
      }
  }

由此可见,feign请求的本质就是requestTemplate,而原理上,就是把sid重新塞到request的header里面.

线程池sid的传递

那么怎么传递到另外的线程里呢? ==答案就是threadLocal作为管道存储对象.

  package com.ess.framework.commons.utils;

  /**
   * 线程变量上下文
   */
  public class EssContextHolder {

      private EssContextHolder() {
      }

      /**
       * sid
       */
      private final static ThreadLocal<String> SID = new ThreadLocal<>();

      /**
       * token
       */
      private final static ThreadLocal<String> TOKEN = new ThreadLocal<>();

      /**
       * 联盟code
       */
      private final static ThreadLocal<String> UNION_CODE = new ThreadLocal<>();

      /**
       * 联盟unionId
       */
      private final static ThreadLocal<String> UNION_ID = new ThreadLocal<>();

      /**
       * 设置SID
       *
       * @param sid
       */
      public static void setSID(String sid) {
          EssContextHolder.SID.set(sid);
      }

      /**
       * 获取SID
       */
      public static String getSID() {
          return EssContextHolder.SID.get();
      }

      /**
       * 设置TOKEN
       *
       * @param token
       */
      public static void setToken(String token) {
          EssContextHolder.TOKEN.set(token);
      }

      /**
       * 获取TOKEN
       */
      public static String getToken() {
          return EssContextHolder.TOKEN.get();
      }

      /**
       * 设置unionCode
       */
      public static void setUnionCode(String unionCode) {
          EssContextHolder.UNION_CODE.set(unionCode);
      }

      /**
       * 获取unionCode
       */
      public static String getUnionCode() {
          return EssContextHolder.UNION_CODE.get();
      }


      /**
       * 设置unionId
       */
      public static void setUnionId(String unionId) {
          EssContextHolder.UNION_ID.set(unionId);
      }

      /**
       * 获取联盟unionId
       */
      public static String getUnionId() {
          return EssContextHolder.UNION_ID.get();
      }
  }

重载线程池实现细节,这样springboot在使用自定义线程池时,就会初始化个性化的实现细节,把sid等会话状态传递到线程的ThreadLocal里.

  package com.ess.framework.boot.asynctask;

  import com.ess.framework.commons.utils.EssContextHolder;
  import org.slf4j.MDC;

  import java.util.concurrent.*;

  /**
   * @ClassName ttt
   * @Description TODO
   * @Author shengfq
   * @Date 2021/5/28 0028 上午 10:53
   * @Version
   */
  public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {


      public ThreadPoolExecutorMdcWrapper(AsyncTaskThreadPoolConfig config,ThreadFactory threadFactory,RejectedExecutionHandler handler ) {
          this(config.getCorePoolSize(), config.getMaxPoolSize(), config.getKeepAliveSecond(), TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(config.getQueueCapacity()),threadFactory,handler);
      }

      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(Runnable task) {
          String sid =   EssContextHolder.getSID();
          String token = EssContextHolder.getToken();
          String unionId = EssContextHolder.getUnionId();
          super.execute(ThreadMdcUtil.wrap(sid,token,unionId,task, MDC.getCopyOfContextMap()));
      }

      @Override
      public <T> Future<T> submit(Runnable task, T result) {
          String sid =   EssContextHolder.getSID();
          String token = EssContextHolder.getToken();
          String unionId = EssContextHolder.getUnionId();
          return super.submit(ThreadMdcUtil.wrap(sid,token,unionId,task, MDC.getCopyOfContextMap()), result);
      }

      @Override
      public <T> Future<T> submit(Callable<T> task) {
          String sid =   EssContextHolder.getSID();
          String token = EssContextHolder.getToken();
          String unionId = EssContextHolder.getUnionId();
          return super.submit(ThreadMdcUtil.wrap(sid,token,unionId,task, MDC.getCopyOfContextMap()));
      }

      @Override
      public Future<?> submit(Runnable task) {
          String sid =   EssContextHolder.getSID();
          String token = EssContextHolder.getToken();
          String unionId = EssContextHolder.getUnionId();
          return super.submit(ThreadMdcUtil.wrap(sid,token,unionId,task, MDC.getCopyOfContextMap()));
      }
  }

mq消息的sid传递

如何传递给mq的消费者呢?实际上有了EssContextHolder对象,在Thread里获取sid很方便,如何通过mq传递sid,可以把sid放入message的header区,在消费端取出来,存入消费端线程的ThreadLocal对象中,就能起到传递的作用.

消息发送

消息接收

  import java.io.IOException;
  import java.nio.charset.StandardCharsets;
  import java.util.Map;
  import java.util.function.Consumer;

  import org.hzero.core.exception.OptimisticLockException;
  import org.hzero.core.message.MessageAccessor;
  import org.springframework.amqp.core.Message;

  import com.rabbitmq.client.Channel;
  import com..mom.me.common.constants.MeBaseConstants;
  import com..mom.me.common.mq.MessageHandler;

  import cn.hutool.core.util.ObjectUtil;
  import cn.hutool.json.JSONObject;
  import cn.hutool.json.JSONUtil;

  import io.choerodon.core.exception.CommonException;
  import io.choerodon.core.oauth.CustomUserDetails;
  import io.choerodon.core.oauth.DetailsHelper;

  import lombok.extern.slf4j.Slf4j;


  /**
   * <p>
   * 消息处理类
   * </p>
   */
  @Slf4j
  public abstract class AbstractMessageHandlers implements MessageHandler {
      private AbstractMessageHandlers() {}

      public static void handleMessage(Message msg, Map<String, Object> headers, Channel channel, Consumer<String> fn) {
          String consumerQueue = msg.getMessageProperties().getConsumerQueue();
          try {
              log.trace("消息队列[{}]处理, headers={}", consumerQueue, headers);
              fn.accept(new String(msg.getBody(), StandardCharsets.UTF_8));
          } catch (CommonException e) {
              log.error("消息队列[{}], 消息处理失败业务异常->{}", consumerQueue,
                              MessageAccessor.getMessage(e.getCode(), e.getParameters(), e.getMessage()).desc());
          } catch (IllegalArgumentException e) {
              log.error("消息队列[{}], 消息处理失败业务异常: {}", consumerQueue, e.getMessage());
          } catch (OptimisticLockException e) {
              // 如果乐观锁异常 则触发重试
              log.warn("消息队列[{}], 数据处理乐观锁异常触发重试", consumerQueue);
              throw e;
          } catch (Exception e) {
              log.error("消息队列[{}], 消息处理失败系统异常", consumerQueue, e);
          }
      }

      /**
       * 携带用户会话信息 -> 消息头获取用户会话信息
       * 
       * 经AbstractMessageSender 发送MQ消息会在消息头附加用户会话信息
       * 
       * JSON示例如下
       * 
       * @param msg
       * @param headers
       * @param channel
       * @param fn
       */
      public static void handleWithUser(Message msg, Map<String, Object> headers, Channel channel, Consumer<String> fn) {
          try {
              // 生产执行发出的mq消息头带有用户会话信息 切勿用于其他系统、模块的mq消息处理
              Object userDetails = headers.get(Constants.HEADER_SID);
              if (ObjectUtil.isNull(userDetails)) {
                  throw new CommonException("MQ消息头获取用户会话信息失败!!!");
              }
              JSONObject jsonObject = JSONUtil.parseObj(userDetails);
              log.trace("设置用户会话信息,传入JSONObject信息: {}", jsonObject);

              CustomUserDetails details = new CustomUserDetails(jsonObject.getStr("username"), MeBaseConstants.DEFAULT);
              details.setTenantId(jsonObject.getLong("tenantId"));
              details.setUserId(jsonObject.getLong("userId"));
              details.setOrganizationId(jsonObject.getLong("organizationId"));
              details.setRealName(jsonObject.getStr("realName"));
              details.setLanguage(jsonObject.getStr("language"));
              details.setAdditionInfo(jsonObject.getJSONObject("additionInfo"));
              DetailsHelper.setCustomUserDetails(details);
              // 设置用户会话异常
          } catch (Exception e) {
              log.error("消息队列[{}], 消息处理失败", msg.getMessageProperties().getConsumerQueue(), e);
              return;
          }
          handleMessage(msg, headers, channel, fn);
      }

      public static void handleWithNack(Message msg, Map<String, Object> headers, Channel channel, Consumer<String> fn) {
          String consumerQueue = msg.getMessageProperties().getConsumerQueue();
          try {
              log.trace("消息队列[{}]处理, headers={}", consumerQueue, headers);
              fn.accept(new String(msg.getBody(), StandardCharsets.UTF_8));
              ack(msg, channel);
          } catch (CommonException e) {
              log.error("消息队列[{}], 消息处理失败业务异常->{}", consumerQueue,
                              MessageAccessor.getMessage(e.getCode(), e.getParameters(), e.getMessage()).desc());
              nack(msg, channel);
          } catch (IllegalArgumentException e) {
              log.error("消息队列[{}], 消息处理失败业务异常: {}", consumerQueue, e.getMessage());
              nack(msg, channel);
          } catch (Exception e) {
              log.error("消息队列[{}], 消息处理失败系统异常:{}", consumerQueue, e);
              nack(msg, channel);
          }
      }

      public static void handleWithUserThrowEx(Message msg, Map<String, Object> headers, Channel channel,
                      Consumer<String> fn) {
          String consumerQueue = msg.getMessageProperties().getConsumerQueue();

          try {
              // 生产执行发出的mq消息头带有用户会话信息 切勿用于其他系统、模块的mq消息处理
              Object userDetails = headers.get(MeBaseConstants.USER_DETAILS_KEY);
              if (ObjectUtil.isNull(userDetails)) {
                  throw new CommonException("MQ消息头获取用户会话信息失败!!!");
              }
              JSONObject jsonObject = JSONUtil.parseObj(userDetails);
              log.trace("设置用户会话信息,传入JSONObject信息: {}", jsonObject);

              CustomUserDetails details = new CustomUserDetails(jsonObject.getStr("username"), MeBaseConstants.DEFAULT);
              details.setTenantId(jsonObject.getLong("tenantId"));
              details.setUserId(jsonObject.getLong("userId"));
              details.setOrganizationId(jsonObject.getLong("organizationId"));
              details.setRealName(jsonObject.getStr("realName"));
              details.setLanguage(jsonObject.getStr("language"));
              details.setAdditionInfo(jsonObject.getJSONObject("additionInfo"));
              DetailsHelper.setCustomUserDetails(details);

              log.trace("消息队列[{}]处理, headers={}", consumerQueue, headers);
              fn.accept(new String(msg.getBody(), StandardCharsets.UTF_8));
          } catch (CommonException e) {
              log.error("消息队列[{}], 消息处理失败业务异常->{}, 详细信息:", consumerQueue,
                              MessageAccessor.getMessage(e.getCode(), e.getParameters(), e.getMessage()).desc(), e);
              throw e;
          } catch (Exception e) {
              log.error("消息队列[{}], 消息处理失败系统异常:{}", consumerQueue, e);
              throw e;
          }
      }

      private static void ack(Message msg, Channel channel) {
          try {
              long deliverTag = msg.getMessageProperties().getDeliveryTag();
              channel.basicAck(deliverTag, false);
          } catch (IOException ioe) {
              log.error("消息处理ack异常{}", ioe.getMessage(), ioe);
          }
      }

      private static void nack(Message msg, Channel channel) {
          try {
              long deliverTag = msg.getMessageProperties().getDeliveryTag();
              channel.basicNack(deliverTag, false, true);
          } catch (IOException ioe) {
              log.error("消息处理nack异常{}", ioe.getMessage(), ioe);
          }
      }

  }

总结

综上所述,sid的设置,拦截,传递,存储都有对应的机制.那么在日志系统中的存储则是通过org.slf4j.MDC;这个对象来实现的.

这个对象的实现机制不在本文中展开细节,后续日志的入库,检索,都是通过slf4j这个日志框架和ELK日志系统进行.本文着重讲解了在应用服务中一个跟踪请求的链路id是如何传递,存储的.

属于抛砖引玉的内容,内容较为主观,希望能对自己和其他开发者在分布式日志调用链路上有点启发.谢谢

参考

SpringBoot + MDC 实现全链路调用日志跟踪
微服务-网关Spring Gateway进阶-日志跟踪唯一ID
日志全链路追踪之MDC
秒杀 : 做一个完善的全链路日志实现方案有多简单

相关推荐
谭震鸿2 小时前
Zookeeper集群搭建Centos环境下
分布式·zookeeper·centos
码上有前5 小时前
解析后端框架学习:从单体应用到微服务架构的进阶之路
学习·微服务·架构
天冬忘忧7 小时前
Kafka 工作流程解析:从 Broker 工作原理、节点的服役、退役、副本的生成到数据存储与读写优化
大数据·分布式·kafka
gjh12087 小时前
什么是微服务?
微服务
IT枫斗者12 小时前
如何解决Java EasyExcel 导出报内存溢出
java·服务器·开发语言·网络·分布式·物联网
求积分不加C12 小时前
Kafka怎么发送JAVA对象并在消费者端解析出JAVA对象--示例
java·分布式·kafka·linq
问窗12 小时前
微服务中Spring boot的包扫描范围
java·spring boot·微服务
GDDGHS_13 小时前
“Kafka面试攻略:核心问题与高效回答”
分布式·面试·kafka
聂 可 以13 小时前
IDEA一键启动多个微服务
java·微服务·intellij-idea
꧁薄暮꧂14 小时前
kafka中的数据清理策略
数据库·分布式·kafka