在实际开发中经常能遇到一些补偿措施使用延迟任务来做,有些可以使用定时任务来实现,但是有些需要有固定的周期规律,所以需要延迟指定时间后搞些事情。
但是呢,本来开发的一个单机很low的一个项目,没有必要引用一些其他的中间件,所以就做了个简单的延迟任务小样,这个是延迟给第三方系统发送通知的业务。
使用的是jdk里面的Delayed作为任务队列,使用过期时间做排序,这里要说一下这并不是简单的链表,实际是带排序的链表。
package xxx;
import lombok.Data;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* 任务信息对象
*/
@Data
public class TaskDelayed implements Delayed {
Integer level; // 等级
long expire; // 过期时间
String data;
String notifyUrl;
/**
* 任务信息构造
* @param level
* @param msg
* @param url
*/
public TaskDelayed(Integer level, String msg,String url) {
this.level = level;
this.expire = System.currentTimeMillis() + TaskUtil.levelMap.get(level);
data = msg;
notifyUrl = url;
}
/**
* 任务升级
* @return
*/
public boolean upgrade(){
this.level++;
if(TaskUtil.levelMap.containsKey(level)){
this.expire = System.currentTimeMillis() + TaskUtil.levelMap.get(level);
return true;
}else{
return false;
}
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
long f = this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS);
return (int) f;
}
}
工具类,在这里面定义了队列,和任务等级,以及任务自动升级的方法
package xxx;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.DelayQueue;
/**
* 单例的任务队列
*/
@Slf4j
@Component
public class TaskUtil {
private DelayQueue<TaskDelayed> delayQueue = new DelayQueue<TaskDelayed>();
/**
* 等级对应延迟时间
*/
public static Map<Integer,Long> levelMap = new HashMap<>();
/**
* 自定义等级对应延迟持剑
*/
static {
levelMap.put(1,1000*0l);
levelMap.put(2,1000*3l);
levelMap.put(3,1000*30l);
levelMap.put(4,1000*300l);
levelMap.put(5,1000*3000l);
}
/**
* 添加任务
* @param task
*/
public void addTask(TaskDelayed task) {
delayQueue.add(task);
}
/**
* 判断是否有需要执行的任务
* @return
*/
public boolean isEmpty() {
return delayQueue.isEmpty();
}
/**
* 获取接下来需要执行的任务
* @return
*/
public TaskDelayed take(){
try {
return delayQueue.take();
} catch (InterruptedException e) {
log.error("delayQueue.take失败",e);
}
return null;
}
/**
* 任务升级到下一个等级,并且仍会丢列中
* @param task
*/
public void upAndAdd(TaskDelayed task) {
if (task.upgrade()) {
this.addTask(task);
}
}
}
下面这些就是启动时间轮用的监听器和时间轮实现类,轮巡获取队列面的任务
package xxx;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
/**
* 延迟任务启动监听器
* 打开@Component生效
*/
//@Component
@Slf4j
public class TaskApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private TaskUtil taskUtil;
@Autowired
private TaskTimeWheel taskTimeWheel;
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
//开启定时任务监听
taskTimeWheel.startWheel();
//添加测试任务
//需要发送通知的时候引入TaskUtil就调用这个方法
// taskUtil.addTask(new TaskDelayed(1, "msg", "url"));
// taskUtil.addTask(new TaskDelayed(2, "msg2", "url2"));
}
}
package xxx;
public interface TaskTimeWheel {
void startWheel();
}
package xxx;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class TaskTimeWheelImpl implements TaskTimeWheel{
@Autowired
private TaskUtil taskUtil;
@Autowired
private TaskNatifyService taskNatifyService;
@Async
@Override
public void startWheel(){
log.info("延迟任务时间轮线程启动");
while (true) {
taskNatifyService.send(taskUtil.take());
}
}
}
下面这个就是具体执行任务的方法了,我这个是发送http请求
package xxx;
public interface TaskNatifyService {
void send(TaskDelayed task);
}
package xxx;
import xxx.utils.HttpUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class TaskNatifyServiceImpl implements TaskNatifyService {
@Autowired
private TaskUtil taskUtil;
/**
* 异步执行延迟任务,执行失败升级任务并扔会队列中
* @param task
*/
@Async
@Override
public void send(TaskDelayed task) {
log.info("执行延时任务:{}", task);
if(task == null){
return;
}
String result = null;
try {
result = HttpUtil.sendHttpRequest(task.getNotifyUrl(), task.getData(), "UTF-8");
if (result!=null && !result.equals("success")) {
log.info("延迟任务发送失败:{}",result);
taskUtil.upAndAdd(task);
}
} catch (Exception e) {
log.error("延时任务执行失败", e);
taskUtil.upAndAdd(task);
}
}
}
这个没有做任务的落库,实际使用最好给log落库,然后项目重启的时候,从库里面把未执行完的任务补偿到队列里面。总之还是比较简陋的,提供个思路罢了。