线上问题整理
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)
- 设置配置项:spring.cloud.stream.bindings.input.consumer.maxAttemps=1
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拉取消息,然后由线程池中的线程并发地处理这些消息。每个线程可能会处理来自不同消息队列的消息。当消息消费失败需要重试时,默认情况下,消息会被重新放回同一个队列中,由于队列和线程的分配关系通常是固定的,所以很可能是同一个线程再次尝试消费该消息。
- 在顺序消费模式下,为了保证消息的顺序性,同一个消息队列中的消息会被同一个线程顺序地处理。如果消费失败,消息也会重新放回队列中,并且在一定的时间后被重新消费,这时候通常还是由同一个线程来消费,以保持消息的顺序性。
- 在RocketMQ客户端,无论是并发消费模式(ConsumeConcurrently)还是顺序消费模式(ConsumeOrderly),都是在客户端内部维护了一个线程池来处理消息的。这个线程池的大小可以通过客户端的配置参数来设置。
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的时候再去校验是否存在
ymlliteflow: parse-mode: parse_one_on_first_exec
Cause analysis 原因分析
- 使用@RefreshScope修饰的Bean作用域就是@Scope("refresh"),与作用域@Scope("prototype")效果一致,即每次获取该Bean时都会创建一个新的实例
- 而这些作用域的Bean都是懒加载,即第一次使用时才会去实例化
- 而LiteFlow启动项目的时候就去校验这些Bean是否存在只能是报错(后续查看源码发现可通过配置调整)