Redis 异步消息队列与延时队列

消息中间件 , 大家都会想到 Rabbitmq 和 Kafka 作为消息队列中间件 , 来给应用程序之间增加异步消息传递功能 。 这两个中间件都是专业的消息队列中间件 , 特性之多超出了大多数人的理解能力 。 但是这种属于重量级的应用 , 使用比较麻烦点 。 如果是轻量级的 , 使用 Redis就可以 。 比如对于那些只有一组消费者的消息队列 , 使用 Redis 就可以非常轻松的搞定 。 Redis 的消息队列不是专业的消息队列 , 它没有非常多的高级特性 , 没有 ack 保证 , 如果对消息的可靠性没有极致的要求 , 那么它可以拿来使用 。
异步消息队列Redis 的 list(列表) 数据结构常用来作为异步消息队列使用 , 使用rpush/lpush操作入队列 , 使用lpop 和 rpop来出队列 。 rpush 和 lpop 结合 或者lpush 和rpop 结合;
Redis 异步消息队列与延时队列文章插图
客户端是通过队列的 pop 操作来获取消息 , 然后进行处理 。 处理完了再接着获取消息 , 再进行处理 。 如此循环往复 , 这便是作为队列消费者的客户端的生命周期 。
问题来了
可是如果队列空了 , 客户端就会陷入 pop 的死循环 , 不停地 pop , 没有数据 , 接着再 pop , 又没有数据 。 这就是浪费生命的空轮询 。 空轮询不但拉高了客户端的 CPU , redis 的 QPS 也会被拉高 , 如果这样空轮询的客户端有几十来个 , Redis 的慢查询可能会显著增多 。通常我们使用 sleep 来解决这个问题 , 让线程睡一会 , 睡个 1s 钟就可以了 。 不但客户端的 CPU 能降下来 , Redis 的 QPS 也降下来了 。
新的问题:
用上面睡眠的办法可以解决问题 。 但是有个小问题 , 那就是睡眠会导致消息的延迟增大 。 如果只有 1 个消费者 , 那么这个延迟就是 1s 。 如果有多个消费者 , 这个延迟会有所下降 , 因为每个消费者的睡觉时间是岔开来的 。有没有什么办法能显著降低延迟呢?你当然可以很快想到:那就把睡觉的时间缩短点 。 这种方式当然可以 , 不过有没有更好的解决方案呢?当然也有 , 那就是 blpop/brpop 。这两个指令的前缀字符b代表的是blocking , 也就是阻塞读 。阻塞读在队列没有数据的时候 , 会立即进入休眠状态 , 一旦数据到来 , 则立刻醒过来 。 消息的延迟几乎为零 。 用blpop/brpop替代前面的lpop/rpop , 就完美解决了上面的问题 。
【Redis 异步消息队列与延时队列】问题喋喋不休:
空闲连接自动断开 你以为上面的方案真的很完美么?先别急着开心 , 其实他还有个问题需要解决 。什么问题?—— 空闲连接的问题 。如果线程一直阻塞在那里 , Redis 的客户端连接就成了闲置连接 , 闲置过久 , 服务器一般会主动断开连接 , 减少闲置资源占用 。 这个时候blpop/brpop会抛出异常来 。所以编写客户端消费者的时候要小心 , 注意捕获异常 , 还要重视 。
消息延时队列 延时队列可以通过 Redis 的 zset(有序列表) 来实现 。 我们将消息序列化成一个字符串作为 zset 的value , 这个消息的到期处理时间作为score , 然后用多个线程轮询 zset 获取到期的任务进行处理 , 多个线程是为了保障可用性 , 万一挂了一个线程还有其它线程可以继续处理 。 因为有多个线程 , 所以需要考虑并发争抢任务 , 确保任务不能被多次执行 。Redis 的 zrem 方法是多线程多进程争抢任务的关键 , 它的返回值决定了当前实例有没有抢到任务 , 因为 loop 方法可能会被多个线程、多个进程调用 , 同一个任务可能会被多个进程线程抢到 , 通过 zrem 来决定唯一的属主 。同时 , 我们要注意一定要对 handle_msg 进行异常捕获 , 避免因为个别任务处理问题导致循环异常退出 。
问题来了:
同一个任务可能会被多个进程取到之后再使用 zrem 进行争抢 , 那些没抢到的进程都是白取了一次任务 , 这是浪费 。 解决办法:Lua是Redis内置脚本 , 执行Lua脚本时 , Redis线程会依次执行脚本中的语句 , 对于客户端来说操作是原子性的 , 将 zrangebyscore 和 zrem 一同挪到服务器端进行原子化操作 , 这样多个进程之间争抢任务时就不会出现这种浪费了 。