【Redis基础和应用】(二)延迟队列

Redis 延迟队列

1. 基本实现

我们平常习惯使用KafkaRabbitMQ作为消息队列中间件,在应用程序之间增加异步消息传递功能。这两个中间件都是专业的消息队列

但是使用RabbitMQ比较复杂,发消息之前要创建Exchange, 再创建Queue, 还要将Queue和Exchange通过某种规则绑定起来,发消息之前要指定routing-key,还需要控制头部信息。消费者在消费消息之前也要进行上面的繁琐过程。在大多数情况下,我们的消息队列只有一组消费者,但是使用RabbitMQ也需要经历上述过程。

对于那些只有一组消费者的消息队列,可以使用Redis的list来实现,但是需要注意的是,Redis的list不是专业的消息队列,没有非常多的高级特性,没有ACK保证。如果对消息的可靠性要求高,则不推荐使用Redis的list作为消息队列。

2. 队列为空问题

客户通过队列的pop()操作来获取消息,处理完数据后,再接着获取消息,如此循环。可是如果队列空了,客户端就会陷入循环等待,没有数据,就要不停的空轮询。空轮询不仅拉高了客户端的CPU消耗,Redis的QPS(每秒查询率, Queries-per-second)性能会降低。

通常来说,使用sleep来解决这个问题。让线程睡一会。通常1s.

阻塞读取

用上面的方法能解决空轮询的问题,但是睡眠会导致延迟增大。为了解决这个问题,就需要使用blpop()/brpop()方法。即blocking阻塞读取。

阻塞度在队列没有数据的时候,会立即进入休眠状态,一旦有数据到来,则立即被唤醒,消息的延迟几乎为0.

空闲连接断开问题

如果线程一直阻塞在等待队列数据中,Redis的客户端连接就成为了闲置连接,闲置过久,服务器就会断开连接,减少闲置资源的占用。这个时候使用blpop()/brpop()方法会抛出异常。所以编写客户端的消费者要小心,如果捕获到异常,还需要重试。

3. 锁冲突处理

分布式锁中,如果客户端在处理请求时加锁失败应该怎么办,一般使用3中策略来处理加锁失败:

  1. 直接抛出异常,通知用户稍后重试

    这种方式比较适合由用户直接发起的请求,用户看到错误的对话框后,会点击重试,这样就起到人工延迟的效果。如果考虑到用户体验,可以由前端代码代替用户进行延迟重试控制。本质上是对当前请求的放弃,由客户端决定是否发起新的请求。

  2. sleep一段时间,然后在重试

    sleep会阻塞当前的消息处理线程,导致队列后续消息处理出现延迟,如果锁冲突的比较频繁或者队列的消息较多,sleep可能并不合适。如果因为个别死锁的Key导致加锁不成功,线程会彻底堵死。后续的消息永远得不到及时处理。

  3. 将请求转移至延迟队列,过一会再重试。延迟队列

    这种方式适合异步消息处理,将当前冲突的请求放入到另一个队列延后处理,以避免冲突

4. 延迟队列的实现

延迟消息队列可以通过zset来实现,将消息序列化成一个字符串作为zset的value,这个消息的到期处理时间为score,然后用多线程轮询zset获取到期的任务进行处理。多线程是为了保证可用性,一旦某一个线程挂掉,还有其他线程可以处理。但是因为有多线程,所以需要考虑并发竞争问题,确保任务不会被多次执行。

[Python版本]

class Message:
    __slots__ = "idx", "data"

    def __init__(self, idx=None, data=None):
        self.idx = idx
        self.data = data


class RedisDemo:
    def __init__(self):
        self.conn_pool = redis.ConnectionPool(host="localhost", port=6379, max_connections=8)
        self.conn = redis.Redis(connection_pool=self.conn_pool, socket_timeout=3000, password="admin")

    def delay(self, msg: Message):
        msg.idx = str(uuid.uuid4())  # 保证value的唯一性
        value = json.dumps(msg)
        retry_ts = time.time() + 5  # 5秒后重试
        self.conn.zadd("delay-queue", retry_ts, value)

    def loop(self):
        while True:
            values = self.conn.zrangebyscore("delay-queue", 0, time.time(), start=0, num=1)
            if not values:
                time.sleep(1)  # 延迟队列为空,sleep一秒后重试
                continue
            value = values[0]
            # 从消息队列中移除消息
            success = self.conn.zrem("delay-queue", value)
            if success:
                msg = json.loads(value)
                print(msg)  # handle msg

Redis的zrem方法是多线程争抢任务的关键,它的返回值决定了当前的实列有没有抢到任务。因为loop()方法可能会被多线程调用,要通过zerm来绝ing唯一的属主。同时要注意对handle_msg()进行异常捕获,避免因为个别任务处理出现问题导致循环异常退出。

进一步优化

同一个任务可能会被多个进程取到之后,在使用zrem进行争抢,那些没抢到的进行就浪费了一次执行,因此可以考虑使用Lua脚本,将zrangebyscore和zrem一同挪到服务器端进行原子化操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

企鹅宝儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值