delayQueue实现

本文探讨了在处理用户订单通知和业务执行失败重试场景中的不同技术方案,包括JavaDelayQueue、RabbitMQ死信队列、RedisZSET以及自研队列。作者分析了各自的优势与缺点,帮助读者在实际项目中选择合适的技术实现.
需求背景
  • 用户下订单成功之后隔20分钟给用户发送上门服务通知短信
  • 订单完成一个小时之后通知用户对上门服务进行评价
  • 业务执行失败之后隔10分钟重试一次

    类似的场景比较多 简单的处理方式就是使用定时任务 假如数据比较多的时候 有的数据可能延迟比较严重,而且越来越多的定时业务导致任务调度很繁琐不好管理。

序号

方案

实现原理

优点

缺点

1

java DelayQueue

wait-notify机制不引入其他服务依赖,wait-notify机制,不做polling,不会浪费cpu。数据保存在JVM内存中,当应用重启会造成数据丢失,或者数据量大时造成DelayQueue过大。
2

RabbitMQ 死信队列

利用ttl 以及DLE (Dead Letter Exchanges)可以模拟出延迟队列

开源,现成的稳定的实现方案每个延迟时间需要单独一个队列(5分钟延时是一个,10分钟延迟是另外一个)。除此以外,使用较复杂,对开发者有一定的要求。
3

redis zset

使用ZSET做优先队列,按照Score维持优先级

设定轮询,每分钟轮询一次zset,找出score小于当前秒数的数据,进行处理,然后将key在zset内删除

比较简单,支持持久化比较简陋的延时队列,有意外报错的话,大不了就多轮询几次。或者等人发现了再解决,也没有ack机制,所以也是挺不靠谱的,使用在不太重要的地方还行
4

自研DelayQueue

执行过程:

参数说明:

  • Topic:一组相同类型Job的集合(队列)。供消费者来订阅。
  • Id:Job的唯一标识。用来检索和删除指定的Job信息。
  • Delay:Job需要延迟的时间。单位:秒。(服务端会将其转换为绝对时间)
  • Body:Job的内容,供消费者做具体的业务处理,以json格式存储。
  • callBack:回调接口地址。使用http协议,该地址是一个url。

存储结构

名称

redis类型

保存数据格式

delayJob状态

FailedDelayJob状态

DelayQzsetjob--->scorepublishedN/A
ReadyQhash tablejobId--->jobreadyready
FailedDelayQzsetjob--->scoreN/Apublished

状态转换说明:

1. 客户端publish一个job,首先会被保存到delayQ中,此时状态为published(DelayJob)

2. 超时后,会从delayQ中取出,放入ReadyQ中,状态为Ready(DelayJob)

3 回调成功,则删除readyQ状态是deleted(DelayJob);

4 回调失败,DelayedJob 变为FailedDelayJob(transfer)删除readyQ信息状态为published(FailedDelayJob)

5.FailedDelayJob超时,会从FailedDelayQ删除,放入ReadyQ,状态为Ready(FailedDelayJob);

6.回调成功,则删除readyQ,状态是deleted(FailedDelayJob);

7.回调失败,小于重试次数,则再次transfer,删除readyQ信息,状态为published(FailedDelayJob);

8.回调失败,超过重试次数,则归档,发提示短信,同时删除readyQ&数据字典信息,deleted(FailedDelayJob)。

DelayJob和FailedDelayJob转换:

 

好的,下面是用DelayQueue实现的代码,注释也比较详细,方便理解: ```java import android.os.Handler; import android.os.Looper; import android.util.Log; import java.util.concurrent.DelayQueue; import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; public class MyDelayQueue { private static final String TAG = "MyDelayQueue"; private static final long DELAY_TIME = 8 * 60 * 1000; // 延迟8分钟 private static final int MAX_SIZE = 6; // 最多6个元素 private DelayQueue<DelayedTask> mDelayQueue = new DelayQueue<>(); // 创建DelayQueue public void addTask(final String taskName) { // 判断队列元素是否超过6个 if (mDelayQueue.size() >= MAX_SIZE) { Log.d(TAG, "addTask: 队列已满"); return; } // 将任务添加到DelayQueue中 mDelayQueue.put(new DelayedTask(taskName, DELAY_TIME)); Log.d(TAG, "addTask: 添加任务 " + taskName); // 开启一个线程用于取出元素并执行任务 new Thread(new Runnable() { @Override public void run() { try { DelayedTask task = mDelayQueue.take(); // 取出元素 Log.d(TAG, "run: 执行任务 " + task.getTaskName()); // 在主线程中执行任务 new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { // TODO: 执行任务逻辑 } }); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } // 实现Delayed接口的任务对象 private class DelayedTask implements Delayed { private String taskName; private long expireTime; public DelayedTask(String taskName, long delayTime) { this.taskName = taskName; this.expireTime = System.currentTimeMillis() + delayTime; } public String getTaskName() { return taskName; } @Override public long getDelay(TimeUnit unit) { return unit.convert(expireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); } @Override public int compareTo(Delayed o) { return Long.compare(this.getDelay(TimeUnit.MILLISECONDS), o.getDelay(TimeUnit.MILLISECONDS)); } } } ``` 在上面的代码中,我们首先定义了DELAY_TIME和MAX_SIZE两个常量,分别表示元素的延迟时间和队列的最大元素个数。然后创建了一个DelayQueue对象mDelayQueue,并定义了addTask方法用于往队列中添加元素。 在addTask方法中,我们首先判断队列元素是否已经达到最大值,如果是则直接返回。否则,将任务添加到DelayQueue中,并开启一个线程用于取出元素并执行任务。在取出元素时,如果队列为空,则线程会阻塞等待元素的到来。 DelayedTask是实现Delayed接口的任务对象,其中的getDelay方法和compareTo方法用于元素的排序和取出。在getDelay方法中,我们通过计算当前时间与元素的过期时间之间的差,得到元素的剩余延迟时间。在compareTo方法中,我们比较两个元素的剩余延迟时间大小,以便DelayQueue可以按照延迟时间的大小对元素进行排序。 最后,在取出元素时,我们在主线程中执行任务的逻辑,这里只是一个TODO,具体的执行逻辑需要根据实际需求来实现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值