场景1
有一个定时任务,需要每次去处理订单表t_order status=0 未处理的数据,处理完成之后将状态status修改为成功1 处理失败后设置为失败2
简单做法
每次按照创建时间顺序,查询100条订单表t_order status=0 未处理的数据,查询到后处理成功的设置为成功 失败的设置为失败
容易出现的问题
1.当订单太多,例如一小时出现了100w条订单,此时容易出现堆积的情况
2.未生成定时任务日志,无法通过日志看出 定时任务的处理速度
3.失败的订单没有重试,当有一段时间由于网络原因所有订单都失败时,这些订单没办法重试
4.此次订单是可以修改订单状态 通过状态去判断是否成功的,如果你访问的是第三方数据库,无法去修改第三方数据库的状态,这种方式则不可取
新方案
假设我们查询的订单t_order是第三方数据库中的,需要通过定时任务去处理这些订单,处理完成后将订单状态修改为成功1 处理失败后设置为失败2
1.新建一张日志表,
CREATE TABLE t_task_log
(
id
bigint NOT NULL AUTO_INCREMENT COMMENT ‘主键id’,
query_start_time
varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ‘定时任务查询条件开始时间’,
query_end_time
varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ‘定时任务查询条件结束时间’,
task_start_time
varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT ‘定时任务开始时间’,
task_end_time
varchar(255) COLLATE utf8mb4_general_ci NOT NULL COMMENT ‘定时任务结束时间’,
total_num
bigint DEFAULT NULL COMMENT ‘此次定时任务总操作条数’,
success_num
bigint DEFAULT NULL COMMENT ‘此次定时任务总成功条数’,
fail_num
bigint DEFAULT NULL COMMENT ‘此次定时任务总失败条数’,
cur_time_task_max_id
varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ‘当前查询时间段 处理数据中的最大任务处理的最大id’,
status
tinyint DEFAULT NULL COMMENT ‘状态 0新建 1完成 2 未完成’,
type
int NOT NULL COMMENT ‘状态(1: 正常日志, 2: 重试日志)’,
repeat_ids
varchar(10000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ‘重复的ids’,
create_time
timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ‘创建时间’,
last_modify_time
timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT ‘最后更新时间’,
PRIMARY KEY (id
),
KEY idx_last_modify_time
(last_modify_time
) USING BTREE,
KEY idx_query_start_time
(query_start_time
) USING BTREE,
KEY idx_query_end_time
(query_end_time
) USING BTREE,
KEY idx_task_start_time
(task_start_time
) USING BTREE,
KEY idx_task_end_time
(task_end_time
) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1871762861052760066 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT=‘定时任务日志表’;
一张订单定时任务数据表
CREATE TABLE t_task_data
(
id
bigint NOT NULL AUTO_INCREMENT COMMENT ‘主键id’,
data_id
varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT ‘查询的订单id’,
status
tinyint DEFAULT NULL COMMENT ‘状态(0新建 3推送失败 4成功)’,
push_count
tinyint DEFAULT NULL COMMENT ‘推送的次数’,
create_time
timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ‘创建时间’,
last_modify_time
timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT ‘最后更新时间’,
PRIMARY KEY (id
),
UNIQUE KEY uniq_data_id
(data_id
) USING BTREE,
KEY idx_last_modify_time
(last_modify_time
) USING BTREE,
KEY idx_status
(status
) USING BTREE,
KEY idx_status_push_doms_count_create_time
(status
,push_doms_count
,create_time
) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=2198 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT=‘推送数据记录表’;
处理流程
@TaskLock(lockTime = 3600)
public void execTask() throws ParseException {
log.info("task定时任务开始执行!");
//获取初始开始时间
String queryStartTime = config.getQueryStartTime();
Integer limitNum = config.getLimitNum();
Integer lessHours = config.getLessHours();
String maxId = null;
if(limitNum>100){
throw new BusinessException("每次推送数量不能超过100");
}
String startTime = queryStartTime ;
String endTime = DateUtil.formatDate(DateUtil.addHours(new Date(), -lessHours), "yyyy-MM-dd HH:mm:ss") ;
//组装定时任务log
TaskLog newTaskLog = new TaskLog();
newTaskLog.setTaskStartTime(DateUtil.formatDate(new Date(), "yyyy-MM-dd HH:mm:ss"));
newTaskLog.setQueryEndTime(endTime);
newTaskLog.setStatus(STATUS_NEW);
//查找最新一条日志
TaskLog taskLog = taskLogMapper.getLogByLastTime();
if(taskLog != null){
//如果不是成功的,则继续执行上一次的任务
if(taskLog.getStatus().intValue() != STATUS_SUCCESS){
startTime = taskLog.getQueryStartTime();
endTime = taskLog.getQueryEndTime();
maxId = taskLog.getCurTimeTaskMaxId();
newTaskLog = taskLog;
log.info("Task 任务 有未完成任务 {}",JSON.toJSONString(newTaskLog));
}else{
startTime = taskLog.getQueryEndTime();
}
}
// 如果是新的
if(newTaskLog != taskLog){
Long totalNumByTimeRange = orderMapper.getTotalNumByTimeRange(startTime, endTime);
if(totalNumByTimeRange==0){
log.info("task未找到需要处理的数据 定时任务结束startTime=" + startTime + " endTime:" + endTime);
return;
}
newTaskLog.setTotalNum(totalNumByTimeRange);
newTaskLog.setSuccessNum(0L);
newTaskLog.setFailNum(0L);
newTaskLog.setQueryStartTime(startTime);
log.info("task 任务 插入新任务,{}" ,JSON.toJSONString(newTaskLog ));
newTaskLog.setTaskEndTime(DateUtil.formatDate(new Date(), "yyyy-MM-dd HH:mm:ss"));
// 插入数据
taskLogMapper.insert(newTaskLog);
}
log.info("task 任务开始: startTime=" + startTime + " endTime:" + endTime );
while(true){
//1.查询需要处理的数据
List<Order> orderList = orderMapper.queryByTimeRange(startTime, endTime,
maxId, limitNum);
if( orderList.size() == 0){
break;
}
Order dto = orderList.get(orderList.size() - 1);
String maxIdTemp = dto.getId();
if(!StringUtils.isEmpty(maxId) && maxIdTemp.compareTo(maxId)<=0){
throw new RuntimeException("maxId 数据异常,请检查是否按照 asc 排序");
}
maxId = maxIdTemp;
//2.做一下幂等,过滤已经处理过的数据
List<Order> effectiveOrderList = filterEffectiveListBuildUpc(pushNikeDomsTaskLog,orderList);
if(!CollectionUtils.isEmpty(effectiveOrderList)){
//将得到的订单进行处理
}
//4.将本次要处理的数据进行落库处理
List<Order> resultDatas = saveDatas(effectiveOrderList);
Long successNum = Long.valueOf(resultDatas.stream().filter(t -> t.getStatus().intValue() == STATUS_SUCCESS).collect(Collectors.toList()).size());
Long failNum = Long.valueOf(resultDatas.stream().filter(t -> t.getStatus().intValue() == STATUS_FAIL).collect(Collectors.toList()).size());
newTaskLog.setSuccessNum(newTaskLog.getSuccessNum()==null?successNum:newTaskLog.getSuccessNum()+successNum);
newTaskLog.setFailNum(newTaskLog.getFailNum()==null?failNum:newTaskLog.getFailNum()+failNum);
}
LambdaUpdateWrapper<TaskLog> updateWrapper=new LambdaUpdateWrapper<>();
updateWrapper.set(TaskLog::getCurTimeTaskMaxId, maxId);
updateWrapper.set(TaskLog::getSuccessNum,newTaskLog.getSuccessNum());
updateWrapper.set(TaskLog::getFailNum,newTaskLog.getFailNum());
updateWrapper.set(TaskLog::getTaskEndTime,DateUtil.formatDate(new Date(), "yyyy-MM-dd HH:mm:ss"));
updateWrapper.set(TaskLog::getRepeatIds,newTaskLog.getRepeatIds());
updateWrapper.eq(TaskLog::getId, newTaskLog.getId());
taskLogMapper.update(null,updateWrapper);
if( effectiveOrderList.size() < limitNum){
break;
}
}
// 更新为成功
LambdaUpdateWrapper<TaskLog> updateWrapper=new LambdaUpdateWrapper<>();
updateWrapper.set(TaskLog::getStatus, STATUS_SUCCESS);
updateWrapper.set(TaskLog::getTaskEndTime,DateUtil.formatDate(new Date(), "yyyy-MM-dd HH:mm:ss"));
updateWrapper.eq(TaskLog::getId, newTaskLog.getId());
pushNikeDomsTaskLogMapper.update(null,updateWrapper);
log.info("Task 定时任务结束执行!");
}