在实际开发中我们经常遇到一些耗时任务 例如:节假日群发短信消息任务,如果是同步去处理该任务,客户端往往会超时,体验很差。为了解决这一问题,设计此任务管理系统。
该系统分为三层次,每个层次对第三方依赖的层度不一样,实现的功能也不一样。
层次一:
最底层的任务管理 不依赖任何其他第三方包(ps:像log4j这样基础的忽略) 实现功能:
1 可以查看正在执行的任务
2 修改任务的执行进度
3 终止正在执行的任务
层次二:
需要用的数据库,这里依赖jfinal 实现功能:
1 可以记录每条数据的记录情况
2 记录执行失败数据 可以重试失败
3 任务支持暂停、继续
4 任务执行的任意阶段对服务器重启 不会影响任务的一致性(每一条数据执行需要做事务控制,没有做事务控制 误差在一条数 据内)
层次三:
业务层...
任务接口设计
/**
* 调度任务接口
* @author xujw
* 2018年12月25日16:56:22
*/
public interface SqqTask {
/**
* 获取任务id
* @return 任务id
*/
String getId();
/**
* 处理任务方法
*/
void run();
/**
* 当前任务向前推进
* @param weight 推进权重
* @param log 说明
*/
void step(int weight,String log);
/**
* 暂停当前任务
* @param msg 暂停原因
*/
void stop(String msg);
/**
* 获取当前任务执行进度权重
* @return
*/
int getTempWeight();
/**
* 获取当前任务的总权重
* @return
*/
int getAllWeight();
/**
* 获取当前执行的日志
* @return
*/
String getTempLog();
/**
* 获取任务的全部执行日志
* @return
*/
String getAllLog();
}
2 提供默认的实现抽象类
public abstract class SqqTaskAbs implements SqqTask ,Serializable {
private static final long serialVersionUID = 1L;
private static final Logger loger = LoggerFactory.getLogger(SqqTaskAbs.class);
private static DecimalFormat df = new DecimalFormat("#.00");
private String id;
//当前任务进度
private int tempWeight = 0;
//当前进度日志
private String tempLog;
//任务的总权重
protected int allWeight = -1;
//任务是否已经停止
private boolean isStop = false;;
//当前任务的所有日志
private StringBuilder allLog = new StringBuilder();
public SqqTaskAbs(String id) {
this.id = id;
}
@Override
public final void stop(String msg) {
isStop = true;
tempLog = msg;
}
@Override
public final void step(int weight, String log) {
SqqAssert.isTrue(!isStop,tempLog);
tempWeight += weight;
tempLog = log;
if(allWeight == -1) {
allWeight = getAllWeight();
SqqAssert.isTrue(allWeight >= 0, "总权重必须大于0");
}
if(allLog.length() > 2018) {
allLog.delete(0, 1024);
}
allLog.append(tempLog+"\r\n");
loger.info("{},当前进度:{}%",log,df.format(100*(1.0*tempWeight/allWeight)));
}
public final String getId() {
return id;
}
public final int getTempWeight() {
return tempWeight;
}
public final boolean isStop() {
return isStop;
}
public final String getTempLog() {
return tempLog;
}
public final String getAllLog() {
return allLog.toString();
}
}
3 任务管理类
/**
* 不支持分布式部署去重
* @author xujw
*/
public class SqqTaskManger {
private static final Logger loger = LoggerFactory.getLogger(SqqTaskManger.class);
private static ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);
private static Hashtable tasks = new Hashtable();
public synchronized static void schedule(final SqqTask task, Date firstTime, long period) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
addTask(task);
}},firstTime,period);
}
public synchronized static void addTask(final SqqTask task) {
final String id = task.getId();
SqqAssert.isNull(tasks.get(id), "该任务正在执行,不能重复添加");
tasks.put(id, task);
fixedThreadPool.execute(new Runnable(){
@Override
public void run() {
try {
task.run();
task.step(0, "任务执行完成!");
}
catch(Exception e) {
loger.error("任务终止",e);
task.stop(e.getMessage());
}
finally {
tasks.remove(id);
}
}
});
}
public synchronized static void stopTask(String id,String msg) {
SqqTask task = tasks.get(id);
if(task != null) {
task.stop(msg);
}
}
public synchronized static SqqTask getTask(String id) {
return tasks.get(id);
}
}
以上实现基础的调度任务 但是有时候无法实现失败重试。
败重试 思路 如下
1 设计两张表 a: sys_task 记录任务的状态 开始执行时间 结束执行时间 成功个数、失败个数.. 结构如下:
b:sys_task_detail 记录每条数据的执行情况 表结构如下:
2 在任务执行前,首先查询到要处理的任务明细,放入数据库表b中
3 执行时从数据库表中取出数据一条一条执行,并记录执行结果,执行成功后可以直接删除(根据业务需要)
为了简化业务代码处理,这里再次做了一次封装,采用装饰模式
这里抽象出三个接口
A addDatail 需要业务系统上报要执行的任务 存入数据库表b中
B doOne 处理一条记录的实现
C getWeight 获取任务的总权重 (ps:这里装饰的目的是,如果任务执行了过程中终止了,再次执行,他的权重不应该是总的 重,应该重新计算再此处计算)要求 每执行一条数据 是一个单位的进度。
public abstract class BusinessTask extends SqqTaskAbs {
private static final long serialVersionUID = 1L;
protected SysTask task = null;
public BusinessTask(SysTask task) {
super(task.getStr("task_id"));
this.task = task;
}
public abstract void addDatail();
public abstract String doOne(SysTaskDetail detail);
public abstract int getWeight();
public int getAllWeight() {
int hadStatus = task.getInt("status");
if(hadStatus == TaskStatus.finish.getStatus()) {
Map map = new HashMap();
map.put("taskId", getId());
map.put("status", TaskDatailStatus.error.getStatus());
Page page = SysTaskDetailService.list(1, 1,task.getStr("tenant_id"), map);
return page.getTotalRow();
}
else {
return getWeight();
}
}
@Override
public void run() {
String tenantId = task.getStr("tenant_id");
int hadStatus = task.getInt("status");
int pageSize = 1000;
Map map = new HashMap();
map.put("taskId", getId());
//任务如果还没有进入运行状态则数据可能是不全的(还在初始化数据的时候服务器重启) 所有要清理下,如果已经到run状态说明数据初始化是没有问题的
if(hadStatus < TaskStatus.run.getStatus()) {
task.set("do_count", 0);
task.set("error_count", 0);
SysTaskDetailService.deleteTask(tenantId, getId());
addDatail();
}
//如果任务已经 是完成状态,再执行该任务认为是失败重试
if(hadStatus == TaskStatus.finish.getStatus()) {
map.put("status", TaskDatailStatus.error.getStatus());
task.set("error_count", 0);
}
else {
map.put("status", TaskDatailStatus.init.getStatus());
}
Page page = SysTaskDetailService.list(1, pageSize,tenantId, map);
//不是失败重试 则总数需要初始化
if(hadStatus != TaskStatus.finish.getStatus()) {
task.set("all_count", page.getTotalRow());
}
task.set("status", TaskStatus.run.getStatus());
task.update();
List list = page.getList();
while(list.size() > 0) {
for(int i=0;i
没有代码给个例子效果图
1 任务列表
2 正在执行的任务
本次设计没有考虑分布式部署情况,任务采用的是本地缓存管理,可以根据自己业务需要稍加改动支持分布式环境。