Skip to content

线上问题整理

Problem

Application version:Java1.8、SpringBoot1.5.x、Spring4.3.x

Problem description

Solution 解决方案

  • x

Cause analysis 原因分析

  • x

循环依赖的问题

Application version:Java1.8、SpringBoot1.5.x、Spring4.3.x

启动项目出现循环依赖异常, 导致项目无法正常启动

Solution 解决方案

  • 异常如果提示的是已有的业务类时,使用@Lazy注解缓解
  • 如果是新的业务类,对于新需求的业务逻辑,尽量拆分一个新的Service来实现,不要在原有的业务类上冗余扩展。
  • 代码重构(后续中台化实现)

Cause analysis 原因分析

  • 项目业务代码结构设计不合理,缺乏扩展性,导致后面接手的人员难以维护。

使用开源的Excel导入组件jxls 导入Excel文件内存溢出

Application version:Java1.8、SpringBoot1.5.x、Spring4.3.x

业务使用导入功能,导入异常的Excel文件(文件来源不清楚),导致系统CPU内存升高,内存溢出。

Solution 解决方案

  • 使用EasyExcel代替jxls(后续中台化 使用异步导入导出方案)
  • 限制请求体大小,使用默认1M即可。
    • 一般十列10000行的表格也不会超过1m,除非是异常文件。
    • 大文件还是通过其他方式导入,例如前端先上传到云空间,使用链接的方式转给后端。

Cause analysis 原因分析

  • 分析得知,该文件仅20条数据却有3.2M大小,其中存在异常对象用特殊方式处理后得到异常对象信息(Debug得知是Excel的Draw对象调试跟踪后得出,使用po解析Excel文件,其将exce转成xml再进行解析,该异常对象解忻的节点数为1500w个数量,后续处理会将该节点数转成xmlbean,进而导致内存溢出。

RocketMQ异常重试次数reconsumeTimes判断失效

Application version:Java1.8、SpringBoot2.3.3、Spring5.2.8

MQ消费异常时,使用平台自研框架提供的ConsumeContext获取当前线程中的MQ上下文。通过该上下文中的MessageExt获取MQ消费重试次数。异常多次重试时,如果是同一个线程消费,获取的重试的次数不正确,导致判断异常。

Solution 解决方案

  • 禁用SpringCloudStream的重试
    • 设置配置项:spring.cloud.stream.bindings.input.consumer.maxAttemps=1
      • 参考源码注释:org.springframework.cloud.stream.binder.ConsumerProperties#maxAttempts(Set to 1 to disable retry)

Cause analysis 原因分析

  • 平台框架和开源框架的实现冲突

  • 平台框架实现MessageListenerConcurrently接口自定义消费逻辑,重写了com.pagoda.platform.cloud.stream.binder.rocketmq.consuming.RocketMQListenerBindingContainer.DefaultMessageListenerConcurrently#consumeMessage方法,里面会调用org.apache.rocketmq.spring.core.RocketMQListener#onMessage方法执行消费逻辑。

  • SpringCloudStream内部会有重试策略,onMessage异常时会根据重试策略进行重试, 默认是三次。是直接在onMessage内部重试三次,而不走上级consumeMessage方法,导致自定义消费逻辑注入的MessageExt不是最新的,onMessage重试期间获取的MessageExt就是一直是同一个MessageExt,根据MessageExt获取的重试次数自然也是同一个次数。

  • 跟是否同一个线程消费无关。

  • 扩展知识:

    • 在RocketMQ客户端,无论是并发消费模式(ConsumeConcurrently)还是顺序消费模式(ConsumeOrderly),都是在客户端内部维护了一个线程池来处理消息的。这个线程池的大小可以通过客户端的配置参数来设置。
      • 在并发消费模式下,客户端会从Broker拉取消息,然后由线程池中的线程并发地处理这些消息。每个线程可能会处理来自不同消息队列的消息。当消息消费失败需要重试时,默认情况下,消息会被重新放回同一个队列中,由于队列和线程的分配关系通常是固定的,所以很可能是同一个线程再次尝试消费该消息
      • 在顺序消费模式下,为了保证消息的顺序性,同一个消息队列中的消息会被同一个线程顺序地处理。如果消费失败,消息也会重新放回队列中,并且在一定的时间后被重新消费,这时候通常还是由同一个线程来消费,以保持消息的顺序性。

LiteFlow组件声明@RefreshScope无法注入的问题

Application version:Java1.8、SpringBoot2.3.3、Spring5.2.8、LiteFlow2.12.0

一个LiteFlow组件,使用@RefershScope注解修饰时,项目启动时抛出异常:

com.yomahub.liteflow.exception.FlowExecutorNotInitException: init flow executor cause error for path [liteflow/*.el.xml],reason: [testProcessCmp] is not exist or [testProcessCmp] is not registered, you need to define a node or chain with id [testProcessCmp] and register it EL: THEN(testProcessCmp,testProcessCmp2);

前置知识:LiteFlow

2019年新出的一个编排业务流程的框架,可以将业务封装为一个个组件去实现,再通过配置文件把这些组件编排起来,达到解耦和优化接口逻辑的目的。

  • 优点:可读性强,可以直接在配置文件中查看整个方法的大体调用链,算是设计模式“责任链”的另一种实现;在抽象程度高的时候,组件、流程均可复用
  • 缺点:不易维护、业务实现代码量增多、小众框架

前置知识:@RefreshScope

@RefreshScope注解是Spring Cloud中的一个注解,用来实现Bean中属性的动态刷新(在不重启项目的情况下刷新@Value注解声明的值)

  • 在POM文件引入相关依赖,在Application.yml文件增加相关配置
  • 后续通过curl -X POST http://localhost:8080/actuator/refresh即可刷新@Value注解的值,

Solution 解决方案

  • 方案1:放弃使用@RefreshScope注解,使用其它方式达成相同效果(在该组件内注入一个@ConfigurationProperties修饰的bean,在这个bean上使用@RefreshScope注解即可)

  • 方案2:application.yml文件增加配置,让LiteFlow启动项目时不要校验Node是否存在,而是使用Node的时候再去校验是否存在

    yml
      liteflow:
        parse-mode: parse_one_on_first_exec

Cause analysis 原因分析

  • 使用@RefreshScope修饰的Bean作用域就是@Scope("refresh"),与作用域@Scope("prototype")效果一致,即每次获取该Bean时都会创建一个新的实例
  • 而这些作用域的Bean都是懒加载,即第一次使用时才会去实例化
  • 而LiteFlow启动项目的时候就去校验这些Bean是否存在只能是报错(后续查看源码发现可通过配置调整)