![4321ea8cc788454da110fa1dc7d28c06.png](https://i-blog.csdnimg.cn/blog_migrate/578c2dbe32eef0210bfe1a8ee43a629b.jpeg)
背景:
当订单⼀一直处于未⽀支付状态时,如何及时的关闭订单,并退还库存?
定时的爆款商品自动上下架、广告的定时更新如何做到?
注:文中架构设计以及实际开发为我司野菱大哥所设计,现入职网易考拉。本文不过是拾人牙慧,将其设计以文字的形式呈现出来而已。
目标:
可靠性:消息进⼊入到延迟队列列后, 保证⾄至少被消费⼀一次。
高可⽤用性:至少得⽀支持多实例例部署。挂掉一 个实例例后,还有后备实例例继续提供服务。
实时性:允许存在⼀一定的时间误 差,希望在秒级。
支持消息删除:业务使⽤用⽅方,可 以随时删除指定消息。
设计方案对比
1. 轮训+DB
原理:定时任务扫描扫描db。 优点:实现简单,保证了可靠性,高可用性,实时性。 缺点:很难实现到细粒度,否则数据库压力比较大,程序CPU很大浪费。
2. RocketMQ
原理:mq自带延迟消费。 优点:实现简单,保证了可靠性,高可用性,实时性,不需要轮训任务,MQ自带实现。 缺点:比较死的粒度,18个level,最大2小时。
3. Redis键过期通知
原理:Redis通过订阅过期的消息,做任务监听。 优点:高可用性,实时性。 缺点:大量键同一时间过期,对redis来说负载大;消息只会发送一次,没有确认机制,不能保证可靠性。
4. 有赞的延时消息:Redis(zset)+轮训
原理:Redis-Zset,无限循环扫描任务Bucket。 优点:高可用性,实时性,持久性。 缺点:独立线程的无限循环,CPU的浪费;如果单点任务多,是否会影响后面其他点的任务准时性;针对超长时间(30天以上)的延迟任务,如果继续用redis,想必是很浪费的。
消息结构
每个Job必须包含一下几个属性:
- Topic:消息的Topic
- Id:Job的唯一标识。用来检索和删除指定的Job信息。
- Delay:Job需要延迟的时间。单位:秒。(服务端会将其转换为绝对时间)
- tag(消息 标签
- msgJson:的内容,供消费者做具体的业务处理,以json格式存储。
具体结构如下图表示:
![e3943f26ccfecbf05332a04cc5f0f6d4.png](https://i-blog.csdnimg.cn/blog_migrate/305cbe272f87dfd8e4099ee55a1e0066.jpeg)
消息状态转换
每个Job只会处于某一个状态下:
- ready:可执行状态,等待消费。
- delay:不可执行状态,等待时钟周期。
- reserved:已被消费者读取,但还未得到消费者的响应(delete、finish)。
- deleted:已被消费完成或者已被删除。
容错机制:
当前节点挂了如何确保任务依然能够无误的执行。
- 采用zookeeper做任务节点集群,当前节点的任务放入redis缓存,key采用『ip:pid』的设计,保证在节点挂掉的情况,有其他节点接手任务,保证了可靠性和可用性。
- 极端情况下,可能多发,需要业务方,保证幂等性。
- 优化:10分钟的业务任务,放入redis缓存List。
附录:
1:JDK Timer(使用案例如HashedWheelTime)
java.util.Timer是一个单线程的定时器,定时调度所拥有的TimerTask任务,TimerTask类是一个定时任务类,实现了Runnable接口,并且是一个抽象类,需要定时执行的任务都需要重写他的run方法
![2458d9dcbb5b39410421078065ad4eeb.png](https://i-blog.csdnimg.cn/blog_migrate/0c8a39af5a60b9fcec2d9ccb28f59103.jpeg)
TaskQueue是一个由平衡二叉树堆实现的优先级队列,每个Timer对象内部都有一个TaskQueue队列,用户线程调用Timer的Schedule方法就是把TimerTask任务添加到TaskQueue队列,在调用schedule方法时,long delay参数用来指明该任务延迟多少时间执行。
TimerThread是具体执行任务的线程,他从TaskQueue队列里面获取优先级最高的任务进行执行,只有执行了当前的任务才会从队列里获取下一个任务,而不管队列是否有任务已经达到了设置的delay时间,一个Timer只有一个TimerThrea线程,因此内部实现为多生产者单消费者模型
1:构建一个定时器
public Timer() {
this("Timer-" + serialNumber());
}
/**
* Creates a new timer whose associated thread may be specified to
* {@linkplain Thread#setDaemon run as a daemon}.
* A daemon thread is called for if the timer will be used to
* schedule repeating "maintenance activities", which must be
* performed as long as the application is running, but should not
* prolong the lifetime of the application.
*
* @param isDaemon true if the associated thread should run as a daemon.
*/
public Timer(boolean isDaemon) {
this("Timer-" + serialNumber(), isDaemon);
}
/**
* Creates a new timer whose