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进行异常捕获, 避免因为个别任务处理问题导致的循环异常退出。