延迟队列实现及其容灾方案
兄弟们好,已经很久没有更新文章了呀,国庆结束以后会逐渐恢复更新的。今天我们来聊一下延迟队列。
在日常的业务开发中,相信大家都遇到过需要让一些任延迟执行的场景,比如与我们息息相关的:
- 京东、美团和淘宝下单以后,若当时没有付款,此时会默认存在一个倒计时,一般是 30 分钟,如果在该时间内你没有付款,则订单取消。
可以发现这个需求本质上就是需要在创建订单以后的 30 分钟以后,释放商品库存等等操作。技术方案就是:实现可以在一定时间以后执行任务的功能。
今天我们要和兄弟们聊的就是可以实现这个功能的一个工具。来看一下今天的大纲
1. 主流实现
延迟队列,延迟一定的时间以后去执行一个任务。大白话来讲:我和一个女孩子表白,如果表白成功,我会带她去吃饭,看电影;如果表白失败,我不会带她去吃饭,看电影。当我表白的以后,我会给他 1 分钟的考虑时间,即在 1 分钟以后,我会根据她的选择,执行不同的任务。
好了,聊完了爱情,我们开始聊技术。(技术不会骗我,爱情会,兄弟们冲)
接下来我们来看目前几种常用的延迟队列的实现方式
1.1 Java 延时队列实现
在 java.util.current
包下提供了很多的并发类,其中就包括很多的同步队列,包括一个延时队列 DelayQueue
。
我们来看一下它的具体用法:(案例在 SpringBoot 环境下运行)
- 实体类,实现了
Delayed
接口,存放了一个 time 属性
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class OaTask implements Delayed {
private String name;
private Long time;
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(time - now(), TimeUnit.MILLISECONDS);
}
private Long now() {
return Instant.now().toEpochMilli();
}
/**
* 根据剩余时间进行排序
* @param o
* @return
*/
@Override
public int compareTo(Delayed o) {
long d = this.getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
return (d == 0) ? 0 : ((d > 0) ? 1 : -1);
}
}
- 延时队列接口
public interface DelayQueueService<E> {
/**
* 向延迟队列中添加任务
* @param e
*/
void add(E e);
/**
* 消费任务
*/
void consume(Consumer<E> consumer);
/**
* 移除任务
*/
void remove(E e);
/**
* 停止延迟队列任务消费
*/
void stop();
}
- 具体实现类
@Component
public class DelayQueueJavaImpl<OaTask extends Delayed> implements DelayQueueService<OaTask> {
@Resource
private DelayQueue<OaTask> delayQueue;
private AtomicBoolean flag = new AtomicBoolean(true);
@Override
public void add(OaTask e) {
delayQueue.add(e);
}
@Override
@SneakyThrows
public void consume(Consumer<OaTask> consumer) {
while (flag.get()) {
// 获取不到任务的时候直接阻塞
OaTask oaTask = delayQueue.take();
consumer.accept(oaTask);
}
}
@Override
public void remove(OaTask oaTask) {
delayQueue.remove(oaTask);
}
@Override
public void stop() {
flag.set(false);
}
}
- 测试用例
@Component
public class DelayQueueJavaTest {
@Autowired
private DelayQueueService<OaTask> queueService;
@PostConstruct
public void init() {
for (int i = 0; i < 10; i++) {
queueService.add(OaTask.builder()
.name("测试"+i+":")
.time(Instant.now().toEpochMilli()+ (i*2*1000))
.build());
}
}
}
简单来解释一下这个案例的原理: Java 对于延迟队列的实现基于 PriorityQueue
优先级队列,将 Delayed
中的 time 属性作为优先级存储在 PriorityQueue
队列中,实现延迟队列的作用。因此我们来使用的时候,需要重写实体类的compareTo
方法。
这种方式可以解决一些简单的业务问题,而且它很轻量,基于 JVM 实现,也正是因为基于 JVM 实现因此它存在一个问题, JVM 有状态。当 JVM 因为内存泄露或者溢出等情况导致程序挂掉,此时内部的数据也出现了丢失,因此给予 JVM 实现的延时队列无法保证数据的高可用。
解决方法不外乎于增加一个备份机制。如数据库。