首页 » SEO优化 » phplist多队列技巧_有赞延迟队列设计

phplist多队列技巧_有赞延迟队列设计

访客 2024-12-09 0

扫一扫用手机浏览

文章目录 [+]

延迟行列步队,顾名思义它是一种带有延迟功能的行列步队。
那么,是在什么场景下我才须要这样的行列步队呢?

背景

我们先看看以下业务场景:

phplist多队列技巧_有赞延迟队列设计

当订单一贯处于未支付状态时,如何及时的关闭订单,并退还库存?如何定期检讨处于退款状态的订单是否已经退款成功?新创建店铺,N天内没有上传商品,系统如何知道该信息,并发送激活短信?等等

为理解决以上问题,最大略直接的办法便是定时去扫表。
每个业务都要掩护一个自己的扫表逻辑。
当业务越来越多时,我们会创造扫表部分的逻辑会非常类似。
我们可以考虑将这部分逻辑从详细的业务逻辑里面抽出来,变成一个公共的部分。
那么开源界是否已有现成的方案呢?答案是肯定的。
Beanstalkd(http://kr.github.io/beanstalkd/), 它基本上已经知足以上需求。
但是,在删除的时候不是特殊方便,须要更多的本钱。
而且,它是基于C措辞开拓的,当时我们团队主流是PHP和Java,没法做二次开拓。
于是我们借鉴了它的设计思路,用Java重新实现了一个延迟行列步队。

phplist多队列技巧_有赞延迟队列设计
(图片来自网络侵删)
设计目标传输可靠性:进入到延迟行列步队后,担保至少被消费一次。
Client支持丰富:由于业务上的需求,至少支持PHP和Python。
高可用性:至少得支持多实例支配。
挂掉一个实例后,还有后备实例连续供应做事。
实时性:许可存在一定的韶光偏差。
支持删除:业务利用方,可以随时删除指定。
整体构造

全体延迟行列步队由4个部分组成:

Job Pool用来存放所有Job的元信息。
Delay Bucket是一组以韶光为维度的有序行列步队,用来存放所有须要延迟的/已经被reserve的Job(这里只存放Job Id)。
Timer卖力实时扫描各个Bucket,并将delay韶光大于即是当前韶光的Job放入到对应的Ready Queue。
Ready Queue存放处于Ready状态的Job(这里只存放Job Id),以供消费程序消费。

如下图表述:

设计要点

基本观点

Job:须要异步处理的任务,是延迟行列步队里的基本单元。
与详细的Topic关联在一起。
Topic:一组相同类型Job的凑集(行列步队)。
供消费者来订阅。

构造

每个Job必须包含一下几个属性:

Topic:Job类型。
可以理解成详细的业务名称。
Id:Job的唯一标识。
用来检索和删除指定的Job信息。
Delay:Job须要延迟的韶光。
单位:秒。
(做事端会将其转换为绝对韶光)TTR(time-to-run):Job实行超时时间。
单位:秒。
Body:Job的内容,供消费者做详细的业务处理,以json格式存储。

详细构造如下图表示:

TTR的设计目的是为了担保传输的可靠性。

状态转换

每个Job只会处于某一个状态下:

ready:可实行状态,等待消费。
delay:不可实行状态,等待时钟周期。
reserved:已被消费者读取,但还未得到消费者的相应(delete、finish)。
deleted:已被消费完成或者已被删除。

下面是四个状态的转换示意图:

存储

在选择存储介质之前,先来确定下详细的数据构造:

Job Poll存放的Job元信息,只须要K/V形式的构造即可。
key为job id,value为job struct。
Delay Bucket是一个有序行列步队。
Ready Queue是一个普通list或者行列步队都行。

能够同时知足以上需求的,非redis莫属了。
bucket的数据构培养是redis的zset,将其分为多个bucket是为了提高扫描速率,降落延迟。

通信协议

为了知足多措辞Client的支持,我们选择Http通信办法,通过文本协议(json)来实现与Client真个交互。
目前支持以下协议:

添加:{‘command’:’add’, ’topic’:’xxx’, ‘id’: ‘xxx’, ‘delay’: 30, ’TTR’: 60, ‘body’:‘xxx'}获取:{‘command’:’pop’, ’topic’:’xxx'}完成:{‘command’:’finish’, ‘id’:’xxx'}删除:{‘command’:’delete’, ‘id’:’xxx'}

body也是一个json串。
Response构造:{’success’:true/false, ‘error’:’error reason’, ‘id’:’xxx’, ‘value’:’job body'} 强调一下:job id是由业务利用方决定的,一定要担保全局唯一性。
这里建议采取topic+业务唯一id的组合。

举例解释一个Job的生命周期

用户对某个商品下单,系统创建订单成功,同时往延迟行列步队里put一个job。
job构造为:{‘topic':'orderclose’, ‘id':'ordercloseorderNoXXX’, ‘delay’:1800 ,’TTR':60 , ‘body':’XXXXXXX’}延迟行列步队收到该job后,先往job pool中存入job信息,然后根据delay打算出绝对实行韶光,并以轮询(round-robbin)的办法将job id放入某个bucket。
timer每时每刻都在轮询各个bucket,当1800秒(30分钟)过后,检讨到上面的job的实行韶光到了,取得job id从job pool中获取元信息。
如果这时该job处于deleted状态,则pass,连续做轮询;如果job处于非deleted状态,首先再次确认元信息中delay是否大于即是当前韶光,如果知足则根据topic将job id放入对应的ready queue,然后从bucket中移除;如果不知足则重新打算delay韶光,再次放入bucket,并将之前的job id从bucket中移除。
消费端轮询对应的topic的ready queue(这里仍旧要判断该job的合理性),获取job后做自己的业务逻辑。
与此同时,做事端将已经被消费端获取的job按照其设定的TTR,重新打算实行韶光,并将其放入bucket。
消费端处理完业务后向做事端相应finish,做事端根据job id删除对应的元信息。
现有物理拓扑

目前采取的是集中存储机制,在多实例支配时Timer程序可能会并发实行,导致job被重复放入ready queue。
为理解决这个问题,我们利用了redis的setnx命令实现了大略的分布式锁,以担保每个bucket每次只有一个timer thread来扫描。

设计不敷的地方

timer是通过独立线程的无限循环来实现,在没有ready job的时候会对CPU造成一定的摧残浪费蹂躏。
消费端在reserve job的时候,采取的是http短轮询的办法,且每次只能取的一个job。
如果ready job较多的时候会加大网络I/O的花费。
数据存储利用的redis,在持久化上受限于redis的特性。
scale-out的时候依赖第三方(nginx)。

未来架构方向

基于wait/notify办法的Timer实现。
供应TCP长连的API,实现push或者long-polling的reserve方法。
拥有自己的存储方案(内嵌数据库、自定义数据构造写文件),确保的持久化。
实现自己的name-server。
考虑供应周期性任务的直接支持。

标签:

相关文章