调度系统设计

前言

在实际开发中我们经常遇到一些耗时任务 例如:节假日群发短信消息任务,如果是同步去处理该任务,客户端往往会超时,体验很差。为了解决这一问题,设计此任务管理系统。

该系统分为三层次,每个层次对第三方依赖的层度不一样,实现的功能也不一样。

层次一:

       最底层的任务管理  不依赖任何其他第三方包(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   记录任务的状态 开始执行时间 结束执行时间 成功个数、失败个数.. 结构如下:

调度系统设计_第1张图片

b:sys_task_detail 记录每条数据的执行情况 表结构如下:

调度系统设计_第2张图片

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 任务列表

调度系统设计_第3张图片

2 正在执行的任务

调度系统设计_第4张图片

总结

本次设计没有考虑分布式部署情况,任务采用的是本地缓存管理,可以根据自己业务需要稍加改动支持分布式环境。

你可能感兴趣的:(学习笔记)