深入Redis(二)延时队列

延时队列

由于平时使用RabbitMQ和Kafka作为消息队列中间件来给程序之间增加异步消息传递功能,但例如RabbitMQ使用时要创建exchange/queue,指定routing-key,还要控制头部消息,如果消息队列只有一组消费者,则显得太繁琐。

那么此时就可以用Redis来代替,前提是对消息可靠性没有严格要求,因为Redis的消息队列不是专业的。

异步消息队列

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

队列空了怎么办

客户端通过pop操作来获取消息,然后再处理,处理完了再获取消息,然后再处理。可如果队列空了,则客户端会陷入pop的死循环,不停地尝试pop,这拉高了客户端的CPU消耗,Redis的QPS也被拉高。

  1. 最容易想到的方法是使用sleep来解决,让线程/进程睡一会儿,1s就可以。

  2. 用1的方法,有个不好的地方就是会导致消息延迟增大(“队列延迟问题”),如果消费者增多,则延迟会下降,想要显著降低延迟,可以使用Redis的blpop/brpop指令,b是阻塞的意思,整体就是阻塞读,阻塞读再没有数据时会休眠,有数据就会立刻醒来。

  3. 用2的方法,虽然完美解决了sleep的问题,但仍然存在线程阻塞(“空闲连接问题”),如果线程一直阻塞,则客户端连接就变成了闲置连接,如果闲置过久则会被服务器主动断开连接,这时blpop/brpop指令会抛异常,因此要注意捕获异常。

锁冲突处理

结合(一)中的分布式锁,如果客户端在处理请求时加锁不成功怎么解决?

  1. 直接抛出异常,通知客户稍后重试,这种方式适合由用户直接发起的请求,本质是放弃当前请求,由用户决定是否重新发起新的请求;
  2. sleep一会后再重试, sleep会阻塞当前消息队列,导致队列后续消息处理出现延迟,如果冲突频繁或队列内消息较多,可能不适合用;
  3. 将请求转移到延迟队列,适合异步消息处理。

延迟队列实现

通过Redis的zset(有序列表)来实现,将消息序列化为字符串作为zset的value,将到期处理时间作为score,用多个线程轮询zset获取到期的任务进行处理,此处要考虑并发冲突。

def delay(msg):
    msg.id = str(uuid,uuid4())
    value = json.dumps(msg)
    retry_ts = time.time() +5
    redis.zadd("delay-queue", retry_ts, value)


def loop():
    while True:
        values = redis.zrangebyscore("delay-queue", 0, time.time(), start=0, num=1)
        if not values:
            time.sleep(1)
            continue
        value = values[0]
        success = redis.zrem("delay-queue", value)
        if success:
            msg = json.loads(value)
            handle_msg(msg)

zrem是多线程多进程争抢消息的关键,其返回值决定了当前线程/进程是否抢到消息,同时handle_msg方法中一定要进行异常捕获,避免个别任务处理出现问题导致循环异常退出。

优化

同一个消息可能被多个进程取到之后再使用zrem进行争抢,因此可以考虑将zrangebyscorezrem挪到服务器端进行原子化操作。

猜你喜欢

转载自www.cnblogs.com/ikct2017/p/9498614.html