β

有赞延迟队列设计

有赞技术团队 63 阅读
有赞延迟队列设计

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

背景

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

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

设计目标

整体结构

整个延迟队列由4个部分组成:

如下图表述: 有赞延迟队列设计

设计要点

基本概念

消息结构

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

具体结构如下图表示: 有赞延迟队列设计 TTR的设计目的是为了保证消息传输的可靠性。

消息状态转换

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

下面是四个状态的转换示意图: 有赞延迟队列设计

消息存储

在选择存储介质之前,先来确定下具体的数据结构:

能够同时满足以上需求的,非redis莫属了。
bucket的数据结构就是redis的zset,将其分为多个bucket是为了提高扫描速度,降低消息延迟。

通信协议

为了满足多语言Client的支持,我们选择Http通信方式,通过文本协议(json)来实现与Client端的交互。 目前支持以下协议:

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

举例说明一个Job的生命周期

现有物理拓扑

有赞延迟队列设计 目前采用的是集中存储机制,在多实例部署时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。
考虑提供周期性任务的直接支持。

作者:有赞技术团队
Thoughts, stories and ideas.
原文地址:有赞延迟队列设计, 感谢原作者分享。

发表评论