分析记录一次长任务的业务模型

目录

1、背景

2、业务流程分析

3、解决方案

3.1 发送每个学生碎片任务消息

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生成完毕回执消息

扫描二维码关注公众号,回复: 12197627 查看本文章

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);
		}

	}

}

猜你喜欢

转载自blog.csdn.net/s2008100262/article/details/112977831