应用开发过程中,我们常常需要用到延时任务的地方,最近在工作遇到了一个需求,用UDP发送报文,发送后30s后要是还没有收到回报报文,就对对于报文进行重发。
类似于订单超时未支付取消订单一样,可以有很多解决方法,我这里采用其中一种,java的延时队列来实现。
用这篇笔记简易记录一下实现过程。
什么是DelayQueue
DelayQueue 是按照元素的延时时间排序的队列。元素必须实现 Delayed 接口,该接口定义了一个 getDelay 方法,用于返回元素的剩余延时时间。
Delayed接口继承了Comparable接口,所以延时队列中的元素对象也必须要实现compareTo方法。
延时队列在内部使用了一个优先级队列(PriorityQueue)来实现,确保队头元素始终是剩余延时时间最小的元素。
实现过程
1. 创建报文内容类
/**
* 报文内容类
* 报文类别号和流水号能确定唯一一条报文
*/
@Data
public class MessageInnerProtocol {
/**
* 报文类别号
*/
private Integer infoClass;
/**
* 流水号
*/
private Integer serialNo;
// 省略其他字段
// ......
}
2. 创建延时队列元素对象
@Getter
public class MessageDelayed implements Delayed {
/**
* 报文内容
*/
private final MessageInnerProtocol message;
/**
* 计时开始时间
*/
private final long startTime;
/**
* 超时时间
*/
private static final long EXPIRE_TIME = 30 * 1000;
/**
* 构造函数
* @param message 报文内容
*/
public MessageDelayed(MessageInnerProtocol message) {
this.message = message;
this.startTime = System.currentTimeMillis();
}
@Override
public long getDelay(TimeUnit unit) {
long elapsedTime = System.currentTimeMillis() - startTime;
return unit.convert(System.currentTimeMillis() - elapsedTime, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
if(this == o){
return 0;
}
// 根据剩余时间来进行排序
long diff = getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS);
if(diff == 0){
return 0;
}else if(diff < 0){
return -1;
}else {
return 1;
}
}
}
3 报文回报状态枚举类
/**
* 回报报文状态
*/
public enum ResultMessageStatusEnums {
/**
* 等待报文回报中
*/
WAITING,
/**
* 成功收到报文回报,且校验成功
*/
SUCCESS,
/**
* 收到回报报文,但是校验发生错误
*/
FAIL;
}
4 创建线程不停的处理超时报文
public class MessageTimeoutDeal {
private static final int KNOWN_CAPACITY = 16;
/**
* 延时队列存储报文
*/
public static final DelayQueue<MessageDelayed> RESULT_MESSAGE_DELAY_QUEUE = new DelayQueue<>();
/**
* <infoClass, <serialNo, ResultMessageStatusEnums>>
* 用保报文类别和流水号,报文状态来对报文进行处理
*/
public static final Map<Integer, Map<Integer, ResultMessageStatusEnums>> RESULT_MESSAGE_MAP = new ConcurrentHashMap<>(KNOWN_CAPACITY);
public static void main(String[] args) {
// 生成报文
MessageInnerProtocol messageInnerProtocol = new MessageInnerProtocol();
// 设置报文内容
messageInnerProtocol.setSerialNo(0x243);
// 流水号建议用 AtomicInteger, 测试简易写一下
messageInnerProtocol.setSerialNo(25);
Map<Integer, ResultMessageStatusEnums> resultMessageMap =
RESULT_MESSAGE_MAP.computeIfAbsent(messageInnerProtocol.getInfoClass(), k -> new ConcurrentHashMap<>(KNOWN_CAPACITY));
// 报文状态放入map中
resultMessageMap.put(messageInnerProtocol.getSerialNo(), ResultMessageStatusEnums.WAITING);
// 将报文加入延时队列
RESULT_MESSAGE_DELAY_QUEUE.add(new MessageDelayed(messageInnerProtocol));
// 创建线程去处理延时队列中的任务
new Thread(() -> {
try {
while (true){
// 从延时队列中获取任务
MessageDelayed messageDelayed = RESULT_MESSAGE_DELAY_QUEUE.take();
MessageInnerProtocol message = messageDelayed.getMessage();
Map<Integer, ResultMessageStatusEnums> resultMessageStatusEnumsMap =
RESULT_MESSAGE_MAP.get(message.getInfoClass());
// 获取到对应报文的回报状态
ResultMessageStatusEnums resultMessageStatusEnums = resultMessageStatusEnumsMap.get(message.getSerialNo());
switch (resultMessageStatusEnums){
case WAITING:
case FAIL:
// 没有收到回报报文或校验和失败
// 需要重发或其他流程
case SUCCESS:
resultMessageStatusEnumsMap.remove(message.getSerialNo());
break;
default:
break;
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
}
}
测试就跳过了,需要报文处理类,对不同报文进行处理,包括回报报文,修改RESULT_MESSAGE_MAP中报文的状态。
需要处理大量的延迟任务, 可以使用netty的时间轮。