目录
3.2 消费来自于que-service的PDF生成完毕回执消息
1、背景
问题背景如下:
我们根据所选择的数据过滤条件,会命中100个学生,平均每个学生20页的PDF,我们生产上会有4个实例来生成PDF文件,平均一个20页的PDF需要15秒,则
100个学生需要(100/4*15)=375秒,所以,我们这个业务“一键生成个性化”需要大概7分钟,如果超过这个时间,则我们的编程模型有问题,所以,问题的关键在于要充分利用
4个机器实例来并行生成PDF,而不是在一个实例上多个线程并发生成PDF,多个机器实例是并行的效率更高,我们要思考如何将这些任务进行分解成多个小任务让多个机器
并行处理。
2、业务流程分析
我的解决思路如下:
1、将过滤条件查询到的数据根据学生的维度进行分解,每个学生一个碎片任务,将任务信息发送到MQ中,并且这个学生碎片任务消息不存在顺序性,相互之间没有依赖关系,所以
通过线程池进行并发发送
2、生产环境上que-service负责生成学生错题PDF文件,有4个que-service实例,这4个实例会并行从MQ上进行消费生成学生错题PDF,同样的每个学生PDF生成完毕会向另一个队列发送
PDF生成完毕回执消息
3、生产环境上homework-api服务会消费PDF生成完毕回执消息,多个实例并行,并且单个实例由线程池进行消费,所以对PDF生成完毕回执消息是并发消费的,每个学生一条回执消息
4、当处理学生PDF生成完毕回执消息时,需要进行并发控制,使用分页式锁保护临界区代码:判断所有学生是否都PDF生成完毕,如果都生成PDF完毕,,则需要合成 ZIP文件。
3、解决方案
下面列出一些关键业务代码,将每个学生碎片任务消息发送到MQ中,以及消费来自于que-service的PDF生成完毕回执消息
3.1 发送每个学生碎片任务消息
/**
*
* 一键生成、重新生成会调用此方法
*
* @param maxNumOfQues
* @param createInfoId
* @param hwpIdList
* @param stuIds
* void
*/
private void sendMsg2QueService(HomeworkClassWrongQueCustomDto homeworkClassWrongQueCustomDto, Long createInfoId,
List<HomeworkStudentsDto> stuDtoList, Map<Long, List<Long>> abPapMap, boolean retryGenerationFlag) {
Assert.notNull(homeworkClassWrongQueCustomDto.getMaxNumOfQues(), "maxNumOfQues为空,不发送信息给QueService");
Assert.notNull(createInfoId, "createInfoId为空,不发送信息给QueService");
Assert.notEmpty(stuDtoList, "stuDtoList为空,不发送信息给QueService");
String hwpIdListStr = this.homeworkClassWrongQueCustomCreateInfoService
.createHwpIdListStr(homeworkClassWrongQueCustomDto);
// 对每一个学生进行异步发送
for (HomeworkStudentsDto stuDto : stuDtoList) {
this.childExecutorService.execute(new Runnable() {
@Override
public void run() {
// step 0:保存学生快照
if (!retryGenerationFlag) {
// homeworkClassWrongQueCustomCreateInfoSlaveStuService.saveStuSnapshot(createInfoId,
// stuDto);
// 必要条件:记数器加 1 ,记录需要完全异步任务个数
String key = RedisKeyPrefixConst.COUNTER_CREATE_INFO_ID + createInfoId;
redisTemplate.opsForValue().increment(key, 1);
redisTemplate.expire(key, 1, TimeUnit.HOURS);
} else {
log.info("重新生成学生快照,createInfoId:{},stuId:{},stuName:{}", createInfoId, stuDto.getStuId(),
stuDto.getName());
}
// step 1:根据hwpIdList、stuId 查询所有错题并组织成tree结构
List<CustomWrongQueDto> customWrongQueDtoList = homeworkClassWrongQueCustomServiceHelper
.findWrongQueV2(hwpIdListStr, stuDto, homeworkClassWrongQueCustomDto, abPapMap);
if (CollectionUtils.isEmpty(customWrongQueDtoList)) {
log.info("该学生没有错题,不需要生成错题本PDF,createInfoId:{},stuId:{},stuName:{}", createInfoId,
stuDto.getStuId(), stuDto.getName());
return;
}
boolean allWrongQueHavingNoVariationFlag = allWrongQueHavingNoVariation(customWrongQueDtoList);
if (HomeworkClassWrongQueCustomCreateInfoTypeEnum.VARIATION.getCode()
.equals(homeworkClassWrongQueCustomDto.getType()) && allWrongQueHavingNoVariationFlag) {
log.info("生成变式题,该学生没有变式题,不需要生成错题本PDF,createInfoId:{},stuId:{},stuName:{}", createInfoId,
stuDto.getStuId(), stuDto.getName());
return;
}
// step 2:设置该学生需要生成pdf错题本
homeworkClassWrongQueCustomCreateInfoSlaveStuService.setNeedCreateWrongPdf(createInfoId,
stuDto.getStuId());
AtomicReference<String> customWrongQueDtoListForString = new AtomicReference<String>();
AtomicReference<String> subjectLiteracyStatListForString = new AtomicReference<String>();
AtomicReference<String> knowledgePointStatListForString = new AtomicReference<String>();
AtomicReference<String> hwpTrendStatListForString = new AtomicReference<String>();
try {
// step 3.1:查询知识点统计:过滤条件,作业最后上传时间为当前时间、当前学生参与的作业(近10次)
List<AttrStat> knowledgePointStatList = homeworkClassWrongQueCustomServiceHelper
.findKnowledgePointStatList(stuDto.getStuId(),
homeworkClassWrongQueCustomDto.getSubjectId());
// step 3.2:查询学科素养统计:过滤条件,作业最后上传时间为当前时间、当前学生参与的作业(近10次)
List<AttrStat> subjectLiteracyStatList = homeworkClassWrongQueCustomServiceHelper
.findSubjectLiteracyStatList(stuDto.getStuId(),
homeworkClassWrongQueCustomDto.getSubjectId());
// step 3.3:查询作业趋势统计:过滤条件,作业最后上传时间为当前时间、当前学生参与的作业(近10次)
List<ForwardAndBackwardMonitorVo> hwpTrendStatList = homeworkClassWrongQueCustomServiceHelper
.findHwpTrendStatList(stuDto.getStuId(), DateUtil.getTimestamp(),
homeworkClassWrongQueCustomDto.getSubjectId());
customWrongQueDtoListForString.set(JSONObject.toJSONString(customWrongQueDtoList,
SerializerFeature.DisableCircularReferenceDetect));
knowledgePointStatListForString.set(
JSONObject.toJSONString(null == knowledgePointStatList ? "" : knowledgePointStatList));
subjectLiteracyStatListForString.set(JSONObject
.toJSONString(null == subjectLiteracyStatList ? "" : subjectLiteracyStatList));
hwpTrendStatListForString
.set(JSONObject.toJSONString(null == hwpTrendStatList ? "" : hwpTrendStatList));
} catch (Exception e) {
log.error("异常:", e);
}
// step 4:发送MQ:正常业务消息
String header = stuDto.getName() + "-" + DateFormatUtils.format(new Date(), "yyyy/MM/dd");
try {
jmsTemplate.send(MqConfig.HOMEWORK_CLASS_WORONG_QUE_CUSTOM_CREATE_INFO_REQUEST, session -> {
MapMessage mapMessage = session.createMapMessage();
mapMessage.setLong("createInfoId", createInfoId);
mapMessage.setLong("stuId", stuDto.getStuId());
mapMessage.setString("stuName", stuDto.getName());
mapMessage.setString("header", header);
mapMessage.setInt("type", homeworkClassWrongQueCustomDto.getType());
mapMessage.setInt("paperAnsIndividuallyGeneratingFlag",
homeworkClassWrongQueCustomDto.getPaperAnsIndividuallyGeneratingFlag());
mapMessage.setString("customWrongQueDtoList", customWrongQueDtoListForString.get());
mapMessage.setString("knowledgePointStatList", knowledgePointStatListForString.get());
mapMessage.setString("subjectLiteracyStatList", subjectLiteracyStatListForString.get());
mapMessage.setString("hwpTrendStatList", hwpTrendStatListForString.get());
log.info(
"异步调用que-service,发送MQ消息,createInfoId:{},stuId:{},stuName:{},type:{},paperAnsIndividuallyGeneratingFlag:{},header:{},mapMessage:{}",
createInfoId, stuDto.getStuId(), stuDto.getName(),
homeworkClassWrongQueCustomDto.getType(),
homeworkClassWrongQueCustomDto.getPaperAnsIndividuallyGeneratingFlag(), header,
JSONObject.toJSONString(mapMessage));
return mapMessage;
});
} catch (Exception e) {
LoggerUtil.error(log, e, "异步调用que-service,发送MQ消息异常,异常上下文,createInfoId:{},stuId:{}",
createInfoId, stuDto.getStuId());
}
}
});
}
// step 5:发送MQ:延迟消息,用于处理appending状态的数据,自动失败,防止长时间que-service不进行响应,超时失败
try {
jmsTemplate.send(MqConfig.HOMEWORK_CLASS_WORONG_QUE_CUSTOM_CREATE_INFO_AUTO_FAIL, session -> {
MapMessage mapMessage = session.createMapMessage();
mapMessage.setLong("createInfoId", createInfoId);
Message msssage = jmsTemplate.getMessageConverter().toMessage(mapMessage, session);
ScheduleMessagePostProcessor postProcessor = new ScheduleMessagePostProcessor(2, TimeUnit.HOURS);
log.info("异步调用que-service,发送MQ延迟消息,超时自动失败,createInfoId:{}", createInfoId);
return postProcessor.postProcessMessage(msssage);
});
} catch (Exception e) {
LoggerUtil.error(log, e, "异步调用que-service,发送MQ延迟消息,超时自动失败,异常上下文,createInfoId:{}", createInfoId);
}
}
3.2 消费来自于que-service的PDF生成完毕回执消息
package com.isatk.yn.module.classwronque.service;
import java.util.concurrent.TimeUnit;
import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Message;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Service;
import com.isatk.yn.MqConfig;
import com.isatk.yn.module.classwronque.dao.HomeworkClassWrongQueCustomCreateInfoRepo;
import com.isatk.yn.module.classwronque.dao.HomeworkClassWrongQueCustomCreateInfoSlaveStuRepo;
import com.isatk.yn.module.classwronque.enums.CreateStatusEnum;
import com.isatk.yn.module.classwronque.model.HomeworkClassWrongQueCustomCreateInfo;
import com.isatk.yn.module.common.enums.YesOrNoEnum;
import com.isatk.yn.module.common.service.RedisLockComponent;
import com.isatk.yn.service.oss.FileCenterService;
import lombok.extern.slf4j.Slf4j;
/**
* @title 班级错题-个性化错题-MQ消息监听器
* @author QiaoLi
* @date 2020-09-12 10:36:08
* @description:
*/
@Slf4j
@Service
public class HomeworkClassWrongQueCustomMQListener {
@Autowired
private HomeworkClassWrongQueCustomCreateInfoService homeworkClassWrongQueCustomCreateInfoService;
@Autowired
private HomeworkClassWrongQueCustomCreateInfoSlaveStuService homeworkClassWrongQueCustomCreateInfoSlaveStuService;
@Autowired
private HomeworkClassWrongQueCustomCreateInfoRepo homeworkClassWrongQueCustomCreateInfoRepo;
@Autowired
private HomeworkClassWrongQueCustomCreateInfoSlaveStuRepo homeworkClassWrongQueCustomCreateInfoSlaveStuRepo;
@Autowired
private FileCenterService fileCenterService;
@Value("${FileCenter_url}")
private String FileCenter_url;
@Autowired
private RedisLockComponent redisLockComponent;
/** 处理que-service中生成,班级错题-个性化-学生pdf生成响应消息 */
@JmsListener(destination = MqConfig.HOMEWORK_CLASS_WORONG_QUE_CUSTOM_CREATE_INFO_RESPONSE , containerFactory = "jmsListenerContainerFactory")
public void consumeForHandleResponse(final Message message) {
MapMessage mapMessage = (MapMessage) message;
try {
Long createInfoId = mapMessage.getLong("createInfoId");
Long stuId = mapMessage.getLong("stuId");
String failedReason = mapMessage.getString("failedReason");
Boolean isSuccessful = mapMessage.getBoolean("isSuccessful");
// data/YJ/image/20200916/1306034805763907584.pdf
String stuPdfDirPath = mapMessage.getString("downloadUrl");
String stuPdfPathForAns = mapMessage.getString("stuPdfPathForAns");
if (Boolean.TRUE.equals(isSuccessful)) {
log.info(
"接收来自【que-service】生成学生错题本的响应消息,createInfoId:{},stuId:{},failedReason:{},isSuccessful:{},downloadUrl:{},stuPdfPathForAns:{}",
createInfoId, stuId, failedReason, isSuccessful, stuPdfDirPath, stuPdfPathForAns);
} else {
log.error(
"接收来自【que-service】生成学生错题本的响应消息,createInfoId:{},stuId:{},failedReason:{},isSuccessful:{},downloadUrl:{},stuPdfPathForAns:{}",
createInfoId, stuId, failedReason, isSuccessful, stuPdfDirPath, stuPdfPathForAns);
}
if (StringUtils.isNotEmpty(failedReason)) {
failedReason = "题库服务(que-service):" + failedReason;
}
HomeworkClassWrongQueCustomCreateInfo createInfo = this.homeworkClassWrongQueCustomCreateInfoRepo
.findOne(createInfoId);
if (null == createInfo) {
log.warn("作业生成记录已经被删除,忽略本次处理,createInfoId:{}", createInfoId);
return;
}
this.updateCreateStatus(createInfoId, stuId, isSuccessful, failedReason, stuPdfDirPath, stuPdfPathForAns);
} catch (Exception e) {
log.error("执行HomeworkClassWrongQueCustomMQListener.consume()异常", e);
} finally {
try {
message.acknowledge();
} catch (JMSException e) {
log.error("手动签收异常", e);
}
}
}
/** 处理que-service中生成,班级错题-个性化错题-超时自动失败 */
@JmsListener(destination = MqConfig.HOMEWORK_CLASS_WORONG_QUE_CUSTOM_CREATE_INFO_AUTO_FAIL
+ "?consumer.prefetchSize=1", concurrency = "1-1", containerFactory = "jmsListenerContainerFactory")
public void consumeForTimeoutAutoFail(final Message message) {
MapMessage mapMessage = (MapMessage) message;
try {
Long createInfoId = mapMessage.getLong("createInfoId");
log.info("接收MQ延迟消息,createInfoId:{}!", String.valueOf(createInfoId));
HomeworkClassWrongQueCustomCreateInfo createInfo = this.homeworkClassWrongQueCustomCreateInfoRepo
.findOne(createInfoId);
if (null == createInfo) {
log.error("createInfo数据库记录被删除,createInfoId:{},本次MQ延迟消息不进行处理", createInfoId);
return;
}
if (CreateStatusEnum.APPENDING.getCode().equals(createInfo.getCreateStatus())) {
log.info("接收MQ延迟消息,超时自动失败,createInfoId:{}!", createInfoId);
this.homeworkClassWrongQueCustomCreateInfoRepo.updateCreateStatus(createInfoId,
CreateStatusEnum.FAILING.getCode(), "接收MQ延迟消息,超时自动失败!");
} else {
log.info("接收MQ延迟消息,忽略本次处理,createInfoId:{},createStatus:{}", createInfoId,
CreateStatusEnum.parse(createInfo.getCreateStatus()));
}
} catch (Exception e) {
log.error("执行HomeworkClassWrongQueCustomService.consume()异常", e);
} finally {
try {
message.acknowledge();
} catch (JMSException e) {
log.error("手动签收异常", e);
}
}
}
private void updateCreateStatus(Long createInfoId, Long stuId, Boolean isSuccessful, String failedReason,
String stuPdfDirPath, String stuPdfPathForAns) {
if (Boolean.FALSE.equals(isSuccessful)) {
this.updateCreateStatusForFailing(createInfoId, stuId, failedReason);
} else if (Boolean.TRUE.equals(isSuccessful)) {
this.updateCreateStatusForSuccess(createInfoId, stuId, stuPdfDirPath, stuPdfPathForAns);
} else {
log.error("isSuccessful值非法:{}", isSuccessful);
}
}
private void updateCreateStatusForFailing(Long createInfoId, Long stuId, String failedReason) {
this.homeworkClassWrongQueCustomCreateInfoRepo.updateCreateStatus(createInfoId,
CreateStatusEnum.FAILING.getCode(), failedReason);
this.homeworkClassWrongQueCustomCreateInfoSlaveStuRepo.updateFailedReason(createInfoId, stuId, failedReason);
this.homeworkClassWrongQueCustomCreateInfoSlaveStuRepo.flush();
}
private void updateCreateStatusForSuccess(Long createInfoId, Long stuId, String stuPdfDirPath,
String stuPdfPathForAns) {
// step 1: 合并学生错题与封面
HomeworkClassWrongQueCustomCreateInfo createInfo = this.homeworkClassWrongQueCustomCreateInfoService
.findOne(createInfoId);
String stuPdfDirPathDownloadUrl = this.fileCenterService.getUrlByPath(stuPdfDirPath);
String mergedStuPdfPath = stuPdfDirPath;
if (YesOrNoEnum.YES.getCode().equals(createInfo.getPrintingCoverFlag())) {
mergedStuPdfPath = this.homeworkClassWrongQueCustomCreateInfoSlaveStuService.mergePdfAndUpload(createInfo,
stuId, stuPdfDirPathDownloadUrl);
}
// step 2: 更新downloadUrl为合并之后的pdf url
this.homeworkClassWrongQueCustomCreateInfoSlaveStuRepo.updateDownloadPath(createInfoId, stuId, mergedStuPdfPath,
stuPdfPathForAns);
this.homeworkClassWrongQueCustomCreateInfoSlaveStuRepo.flush();
// step 3: 判断所有的学生pdf 是否都合并完毕?
boolean isAllStuPdfMerged = this.homeworkClassWrongQueCustomCreateInfoService.isAllStuPdfMerged(createInfo);
if (!isAllStuPdfMerged) {
return;
}
//为了将互斥区变小,将isAllStuPdfMerged的判断放到外面
String lockKey = "updateCreateStatusForSuccess." + createInfoId + "." + "lock";
try {
boolean tryLockFlag = redisLockComponent.tryLock(lockKey, 900, TimeUnit.SECONDS);
if (!tryLockFlag) {
return;
}
log.info("该作业生成信息,,createInfoId:{},所有学生错题本已经生成,正在进行合成zip包......", createInfo.getId());
this.homeworkClassWrongQueCustomCreateInfoService.generateZip(createInfo);
} catch (Exception e) {
log.error("异常", e);
} finally {
this.redisLockComponent.releaseLock(lockKey);
}
}
}