背景:用户群体不断增加,部门不断扩展,产品经理提出越来越多的活动运营需求,程序员不堪重负。但是这些活动做多做久了,很容易发现这些活动往往都有相同的触达体系、任务体系及奖励体系。
为加快产品迭代,减轻技术人员负担,提示系统安全稳定性。独立活动中台、任务中台、营销触达中心迫在眉睫。本文主要阐述任务中台搭建流程。
解决问题:
1 构建更易扩展的任务场景数据库设计及代码设计;
2 庞大会员体系(亿+)快速支撑任务开启、任务校验所涉及的并发及压力问题;
3 依靠服务器及数据库水平扩容保证更大压力下的任务中台支撑能力;
技术方案:
1 采用策略模式设计任务中台,校验不同任务完成状况,并更新用户任务记录表。
数据库设计见下图:
2 使用redis并发锁,通过锁定用户userID 3秒解决并发问题。使用hystrix熔断降级,保证服务能力。
3 任务调度系统采用SaturnJob分布式调度系统,数据库为Mycat+mysql集群,通过Saturn分片参数,执行不同分片数据库数据,支撑服务能力水平提升。
实施:
1 数据库设计见下图:
2 依靠策略模式核心代码:
/**
* 策略类
*/
public abstract class QuestStrategy {
@Autowired
private TaskRecordManager taskRecordManager;
/**
* 对接关联系统校验任务是否完成
*
* @return
*/
public abstract boolean checkQuestIsComplete(TaskRecord record);
}
public enum QuestHandlerEnum {
//等级提升
MEMBER_GRADE_QUEST("0502", ServiceBean.getSpringContext().getBean(MemGradeStrategy.class)),
//商品兑换
MEMBER_EXCHANGE_QUEST("0503", ServiceBean.getSpringContext().getBean(MemExchangeStrategy.class)),
//页面浏览
BROWSE_QUEST("06", ServiceBean.getSpringContext().getBean(PageStrategy.class)),
//页面点击
CLICK_QUEST("07", ServiceBean.getSpringContext().getBean(PageStrategy.class));
private String type;
private QuestStrategy questStrategy;
QuestHandlerEnum(String type, QuestStrategy quest) {
this.type = type;
this.questStrategy = quest;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public QuestStrategy getQuestStrategy() {
return questStrategy;
}
}
public class QuestHandler {
private static final Logger logger = LoggerFactory.getLogger(LoggerType.COMMON);
static Map<String, QuestStrategy> strategyMap = new HashMap<>();
static {
for(QuestHandlerEnum questEnum : QuestHandlerEnum.values()){
strategyMap.put(questEnum.getType(),questEnum.getQuestStrategy());
}
}
public static QuestStrategy getStrategy(String type) {
return strategyMap.get(type);
}
/**
* 校验任务是否完成
* @return
*/
public static TaskRecord checkQuest(String type, TaskRecord taskRecord,boolean isJob){
try {
QuestStrategy questStrategy = getStrategy(type);
//校验任务完成状态
boolean isFinish;
if(isJob){
isFinish = questStrategy.checkQuestIsCompleteJob(taskRecord);
}else{
isFinish = questStrategy.checkQuestIsComplete(taskRecord);
}
//未完成 将任务列表返回
if(!isFinish){
return taskRecord;
}
//任务完成 通过userId 和ID更新任务状态
taskRecord.setStatus(Constant.RecordStatus.FINISHED);
questStrategy.insertOrUpdateTask(taskRecord);
return taskRecord;
} catch (Exception e) {
logger.error("checkQuest error ", e);
}
return taskRecord;
}
}
/**
* 等级提升实现类
*/
@Service
public class MemGradeStrategy extends QuestStrategy {
@Autowired
private IntegralServiceImpl integralService;
private static final Logger logger = LoggerFactory.getLogger(LoggerType.COMMON);
@Override
public boolean checkQuestIsComplete(TaskRecord record) {
//查询会员等级变化情况
QueryGradeChangeDTO queryGradeChangeDTO = new QueryGradeChangeDTO();
queryGradeChangeDTO.setFlag("1");
queryGradeChangeDTO.setUserId(record.getUserId());
queryGradeChangeDTO.setActivityStartTm(record.getStartTm());
MemberGradeChangeVO memberGradeChange = integralService.getChangeInfo(queryGradeChangeDTO);
if(null == memberGradeChange) {
return false;
}
updateTaskProcess(record,String.valueOf(memberGradeChange.getGradeCode()));
String value = getTargetString(record.getRule());
return memberGradeChange.getGradeCode().compareTo(value)>=0 && memberGradeChange.getGradeCode().compareTo(memberGradeChange.getLstGradeCode())>0 && value.compareTo(memberGradeChange.getLstGradeCode())>0;
}
}
/**
* 分布式调度任务执行
*/
@Component
public class UpdateTaskStatusJob extends AbstractSaturnJavaJob {
private static final int mysqlNodeCount = 128;
private static final Logger logger = LoggerFactory.getLogger(LoggerType.COMMON);
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
private final static List<Integer> slicingList = new ArrayList<>();
@Autowired
TaskRecordManager taskRecordManager;
@Autowired
TaskRecordService taskRecordService;
static {
for (int i = 1; i <= mysqlNodeCount; i++) {
slicingList.add(i);
}
}
/**
* Java作业处理入口
*
* @param jobName 作业名
* @param shardItem 分片项
* @param shardParam 分片参数
* @param shardingContext 其它参数信息
* @return 返回执行结果
* @throws InterruptedException 注意处理中断异常
*/
@Override
public SaturnJobReturn handleJavaJob(String jobName, Integer shardItem, String shardParam,
SaturnJobExecutionContext shardingContext) {
logger.info("UpdateTaskStatusJob定时任务执行--开始");
long start = System.currentTimeMillis();
SaturnJobReturn jr = new SaturnJobReturn();
try {
List<Integer> shardIds;
if (StringUtils.isBlank(shardParam)) {
shardIds = slicingList;
} else {
Integer piece = Integer.valueOf(shardParam);
shardIds = slicingList.stream().collect(Collectors.groupingBy(e->e%shardingContext.getShardingTotalCount())).get(piece);
}
for (Integer shardId : shardIds) {
processShard(shardId);
}
jr.setReturnCode(SaturnSystemReturnCode.SUCCESS);
logger.info("UpdateTaskStatusJob定时任务执行--结束 耗时:{}ms", System.currentTimeMillis() - start);
}catch (Exception e){
logger.error("UpdateTaskStatusJob定时任务执行 error:{}",slicingList,e);
jr.setReturnCode(SaturnSystemReturnCode.SYSTEM_FAIL);
}
return jr;
}
private void processShard(Integer shardId) {
try {
logger.info("开始处理分片数据 分片id:{}",shardId);
long start = System.currentTimeMillis();
long total = 0;
Integer lastId = 0;
List<TaskRecord> taskRecordList = taskRecordManager.findTaskRecordByShard(shardId, lastId, 1000);
while (!taskRecordList.isEmpty()) {
total += taskRecordList.size();
processData(taskRecordList);
lastId = taskRecordList.get(taskRecordList.size() - 1).getId();
taskRecordList = taskRecordManager.findTaskRecordByShard(shardId, lastId, 1000);
}
logger.info("处理分片数据结束 分片id:{} total:{} 耗时:{}ms", shardId,total, System.currentTimeMillis() - start);
} catch (Exception e) {
logger.error("定时任务处理分片数据失败,分片id:{}", shardId, e);
}
}
private void processData(List<TaskRecord> taskRecordList) {
executorService.submit(() -> {
if (!CollectionUtils.isEmpty(taskRecordList)) {
TaskCheck taskCheck;
for (TaskRecord taskRecord : taskRecordList) {
taskCheck = new TaskCheck();
if(taskRecordService.checkInValid(taskRecord)){
logger.info("任务已经失效。record:{}", JSONObject.toJSONString(taskRecord));
continue;
}
taskCheck.setMobile(taskRecord.getMobile());
taskCheck.setIds(taskRecord.getTaskCode());
taskCheck.setScene(taskRecord.getScene());
taskCheck.setUserId(taskRecord.getUserId());
taskRecordService.checkTaskJob(taskCheck);
}
}
});
}
}
3 部分运行截图: