Redis 延时队列

Redis 延时队列

Redis的消息队列不是专业的消息队列, 没有非常多的高级特性, 没有ack保证, 如果对消息的可靠性有极致的追求, 那么它就不适合使用。

异步消息队列

Redis的list(列表)数据结构常用来作为异步消息队列使用,使用rpush/lpush操作入队列, 使用lpop和rpop出队列。

队列空了怎么办

客户端是通过队列的pop操作来获取消息,然后进行处理,处理完了在接着获取消息,在进行处理,如此循环往复,这便是队列消费者的客户端生命周期。

如果队列空了,客户端就会陷入pop的死循环,不停pop,没有数据,在pop,又没有数据,这就是空轮询。空轮询不仅拉高了客户端的cpu,Redis的QPS也会被拉高,这样Redis的慢查询也会增多。

通常我们使用sleep来解决这个问题,不但客户端的cpu能降下来,Redis的QPS也能降下来。

队列延迟

用睡眠的方法可以解决上述队列空的问题,但是有个小问题,那就是睡眠会导致消息的延迟增大,如果只有1个消费者,延迟1s。如果有多个消费者,延迟会有所下降,因为每个消费者的睡眠时间是岔开的。

有没有什么方法能显著降低延迟呢?那就是使用blpop/brpop指令。

这两个指令的前缀字符b代表的是blocking,也就是阻塞读。

阻塞读在队列没有数据时,会立即进入睡眠状态,一旦数据到来,则立即醒过来。消息的延迟几乎为0。

空闲连接自动断开

上面的队列延迟的方案还有一个问题需要解决。

什么问题? 那就是空闲连接的问题。

如果线程一直阻塞在哪里,Redis客户端连接就成了闲置连接,闲置过久,服务器一般会主动断开连接,减少闲置资源的占用,这个时候blpop/brpop就会抛出异常出来。

所以编写客户端消费者时要小心,注意捕获异常,还要重试。...

延迟队列的实现

延时队列可以通过Redis的zset(有序列表)来实现。我们将消息序列化为一个字符串作为zset的值。这个消息的到期时间处理时间作为score,然后用多个线程轮询zset获取到期的任务进行处理,多线程时为了保障可用性,万一挂了一个线程还有其他线程可以继续处理。因为有多个线程,所有需要考虑并发争抢任务,确保任务不能被多次执行。

def delay(msg):
    # 保证value唯一
    msg.id = str(uuid.uuid4())
    value = json.dumps(msg)
    # 5s后重试
    retry_ts = time.time() + 5
    redis.zadd("delay-queue", retry_ts, value)
    
def loop():
    while True:
        # 最多取1条
        values= redis.zrangebyscore("delay-queue", 0, time.time(), start=0, num=1)
        if not values:
            # 延时队列空的,休息1s
            time.sleep(1)
            continue
        value = values[0]
        success = redis.zrem("delay-queue", value)
        if success:
            msg = json.loads(value)
            # 处理消息逻辑函数
            handle_msg(msg)

def handle_msg(msg):
    """消息处理逻辑"""
    pass

Redis的zrem方法是对多线程争抢任务的关键,它的返回值决定了当前实例有没有抢到任务,因为loop方法可能会被多个线程、多个进程调用, 同一个任务可能会被多个进程线程抢到,通过zrem来决定唯一的属主。

同时,一定要对handle_msg进行异常捕获, 避免因为个别任务处理问题导致的循环异常退出。

posted @ 2020-08-31 09:55  phper-liunian  阅读(389)  评论(0编辑  收藏  举报