最近在公司做需求时,发现自己所负责的另一个系统中的某个方法报TP50频率较高:
TP50=1037ms[偏差245.66%],超过10次TP50>=300ms
这里的TP50其实是个方法性能监控指标
:在一个时间段内(如5分钟),统计该方法每次调用所消耗的时间,并将这些时间按从小到大的顺序进行排序,取第50%的那个值作为TP50值;配置此监控指标对应的报警阀值后,需要保证在这个时间段内该方法所有调用的消耗时间至少有50%的值要小于此阀值,否则系统将会报警。
趁着最近也有技改的需求,顺带优化下该方法,避免一直出现报警。于是,开始进行排查:
因此,采用非递归
的方式进行优化,即:空间换时间
其实这种问题在java出参封装中很常见,包括:多级菜单、多级题目等等。所以,我在这里进行一个简单的总结,具体解决方法可见2.4
在Java中实现多级菜单DTO的出参封装,一般使用递归或非递归两种方式
创建一个DTO类,于表示菜单项,包含菜单项的名称、ID、type、title、子菜单列表等属性。
定义一个栈stack,用于保存待处理的菜单实体。
首先将一级菜单实体入栈(栈中保存的内容视场景决定,有可能是id、有可能是某个实例)。
进入循环,直到栈为空为止:
返回结果集
将数据库查到的问题集合Question映射成问题QuestionDTO集合。这里的Question有一个属性为type字段,用于表示当前题目是否是父题目
public class Question extends AbstractEntity {
/**
* 对应题库编号
*/
private Long paperId;
/**
* 题目序号
*/
private Long questionIndex;
/**
* 段落编号
*/
private Long sectionId;
private Long knowledgeId = 0l;
private String knowledgeName;
/**
* 知识库链接[旧],用于关联知识点后,编辑试题时展示
*/
private String knowledgeLink;
/**
* 问卷相关内容
*/
private Long dimensionId;
private String dimensionName;
private Long subDimensionId;
private String subDimensionName;
private String hasElse;
/**
* 试题类型
* 单选,多选,情景
*/
private Integer type;
/**
* 分数
*/
private Integer score;
/**
* 题目
*/
private String title;
/**
* 解析
*/
private String solution;
/**
* 知识库链接[新],用于考试结束展示作答记录以及练习模式显示
*/
private String knowLink;
/**
* 情景试题 ---> 子试题
*/
private Long parentId = 0L;
private transient Long tempId;
private List<Answer> answers;
private List<Question> children = new ArrayList<Question>();
private List<QuestionAttachment> questionAttachments = new ArrayList<QuestionAttachment>();
private Boolean skip;
private Long targetQuestionId;
private Long targetQuestionIndex;
private Long usedPaper=0L;
}
public class AbstractEntity implements Serializable {
private Long id;
@JsonSerialize(
using = Json.DateTimeMinuteSerializer.class
)
private Date createdAt;
private String createdPin;
@JsonSerialize(
using = Json.DateTimeMinuteSerializer.class
)
private Date updatedAt;
private String updatedPin;
private Integer sysVersion;
private Boolean yn;
}
public class QuestionDTO implements Serializable {
private Long id;
private List<QuestionDTO> children;
private List<AnswerDTO> answerDTOs;
private Long questionIndex;
/**
* 题目
*/
private String title;
/**
* 试题类型
* 单选,多选,简答
*/
private Integer type;
private Boolean skip;
private Long targetQuestionId;
}
/**
* List 转换question 成 questionDTO
* @param questions
* @return
*/
public List<QuestionDTO> convertQuestions2DTO(List<Question> questions){
List<QuestionDTO> questionDTOs = new ArrayList<QuestionDTO>();
for(Question question : questions){
QuestionDTO questionDTO = convert2QuestionDTO(question);
questionDTOs.add(questionDTO);
}
return questionDTOs;
}
/**
* 转换 question 2 questionDTO
* @param question
* @return
*/
private QuestionDTO convert2QuestionDTO(Question question){
QuestionDTO questionDTO = new QuestionDTO();
BeanUtils.copyProperties(question,questionDTO);
// 当前问题是父问题,递归寻找该问题的所有子问题,封装为questionDTOs
if(SurveyTypeEnum.PARENT.getCode().equals(question.getType())){
List<QuestionDTO> questionDTOs = convertQuestions2DTO(question.getChildren());
questionDTO.setChildren(questionDTOs);
}else{
// 当前问题不是父问题,直接设置当前问题的答案属性
List<AnswerDTO> answerDTOs = convertAnswer2DTO(question);
questionDTO.setAnswerDTOs(answerDTOs);
}
return questionDTO;
}
/**
* 转换 list answer 2 answerDto
* @param question
* @return
*/
private List<AnswerDTO> convertAnswer2DTO(Question question){
List<AnswerDTO> answerDTOs = new ArrayList<AnswerDTO>();
for(Answer answer : question.getAnswers()){
AnswerDTO answerDTO = new AnswerDTO();
BeanUtils.copyProperties(answer,answerDTO);
answerDTOs.add(answerDTO);
}
return answerDTOs;
}
在非递归实现中,我使用了一个栈来保存待处理的问题对象question。
/**
* 使用非递归方式进行问题集合DTO的封装
* @param questions 问题集合
* @return 问题实例进行一一映射
*/
private List<QuestionDTO> convertQuestions2DTOByNonRecursion(List<Question> questions) {
List<QuestionDTO> questionDTOs = new ArrayList<>();
Stack<Question> stack = new Stack<>();
// 自定义线程安全的栈
ConcurrentStack<QuerySurveyDTO> concurrentStack = new ConcurrentStack<>();
// 问题实体依次入栈
for (Question question : questions) {
stack.push(question);
}
// 栈为空,表示封装结束
while (!stack.isEmpty()) {
Question question = stack.pop();
QuestionDTO questionDTO = new QuestionDTO();
BeanUtils.copyProperties(question, questionDTO);
// 如果当前问题是父问题,创建子问题集合childQuestionDTOs。
// 遍历当前问题实体的所有子问题,将每个子问题依次入栈
// 并且映射成一个dto, 加入到子问题集合中childQuestionDTOs,全部遍历结束设置父问题属性children为childQuestionDTOs
if (SurveyTypeEnum.PARENT.getCode().equals(question.getType())) {
List<QuestionDTO> childQuestionDTOs = new ArrayList<>();
for (Question childQuestion : question.getChildren()) {
stack.push(childQuestion);
QuestionDTO childQuestionDTO = new QuestionDTO();
BeanUtils.copyProperties(childQuestion, childQuestionDTO);
childQuestionDTOs.add(childQuestionDTO);
}
questionDTO.setChildren(childQuestionDTOs);
} else {
// 如果当前问题不是父问题,则遍历当前问题的答案集合,逐一封装成answerDTO,加入到answerDTOs集合。注入到问题DTO的answerDTOs属性中
List<AnswerDTO> answerDTOs = new ArrayList<>();
for (Answer answer : question.getAnswers()) {
AnswerDTO answerDTO = new AnswerDTO();
BeanUtils.copyProperties(answer, answerDTO);
answerDTOs.add(answerDTO);
}
questionDTO.setAnswerDTOs(answerDTOs);
}
questionDTOs.add(questionDTO);
}
return questionDTOs;
}
虽然Stack
继承了Vector
,其pop
、peek
方法加了synchronized
关键字,是线程安全的,但官方并不建议继续使用用Stack。
因此,在并发场景下,我使用了线程安全队列ConcurrentLinkedQueue
来实现栈的功能:使用一个队列实现栈,需要注意的是,在元素element每次入队时,需要将除当前元素element之外的所有元素,都依次出队,再入队。那么,最新加入的元素element又跑到队头去了,保证栈的先进后出特性。
/**
* 使用一个线程安全的队列ConcurrentLinkedQueue实现栈
* @param 数据类型
*/
static class ConcurrentStack<T>{
Queue<T> queue;
public ConcurrentStack() {
this.queue = new ConcurrentLinkedQueue<>();
}
public void push(T element){
queue.offer(element);
int size = queue.size();
while(size-- > 1){
queue.offer(queue.poll());
}
}
public T pop(){
return queue.poll();
}
public boolean isEmpty(){
return queue.isEmpty();
}
}