Redis 剖析 List 信息队列的三种消费线程模型

Redis 列表(List)是一种便捷的字符串列表,它的底层成功是一个双向链表。

消费环境,很多公司都将 Redis 列表运行于轻量级信息队列 。这篇文章,咱们聊聊如何经常使用 List 命令成功信息队列的配置以及剖析消费者线程模型 。

一、外围流程

消费者经常使用 LPUSH key element[element...] 将信息拔出到队列的头部,假设 key 不存在则会创立一个空的队列再拔出信息。

如下,消费者向队列 queue 先后拔出了 「Java」「勇哥」「Go」,前往值示意信息拔出队列后的个数。

> LPUSH queue Java 勇哥 Go(integer) 3

消费者经常使用 RPOP key 依次读取队列的信息,先进先出,所以 「Java」会先读敞开费:

> RPOP queue"Java"> RPOP queue"勇哥"> RPOP queue"Go"

接上去,咱们可以经过 spring-data-redis API 演示消费消费流程:

redisTemplate.opsForList().leftPush("queue" , "Java");redisTemplate.opsForList().leftPush("queue" , "勇哥");redisTemplate.opsForList().leftPush("queue" , "Go");

咱们启动一个独立的线程从队列中读取信息(RPOP 命令),读取成功之后,消费信息,若没有信息,则休眠一会,下一次性循环再继续。

上图的伪代码中, while(true) 循环内不停地调用 RPOP 指令,当有信息时,可以及时处置,但假设没有读取到信息,则须要休眠一会。

这里要加休眠,关键是为了缩小空读的频率,防止 CPU 有意义的消耗。

有什么更优化的方式吗?有,那就是经常使用 Redis 阻塞读取 List 的命令。

Redis 提供了 BLPOP、BRPOP 阻塞读取的命令,消费者在在读取队列没有数据的时智能阻塞,直到有新的信息写入队列,才会继续读取新信息口头业务逻辑。

BRPOP queue 0

参数 0 示意阻塞期待期间有限度 。

如图,咱们启动一个消费线程永动机,消费线程拉取信息后,口头消费逻辑。

这种消费者线程模型十分容易了解,同时也十分适宜顺序消费的形式。同时,假设咱们在消费信息时,主机宕机或许断电,或许失落一条信息。

接上去,咱们想一想,有没有消费速度更高的消费模型吗?笔者依据过往的教训,罗列三种形式:

二、拉取线程 + 消费线程池(非阻塞形式)

为了优化消费速度,咱们可以将拉取和消费拆分红两种举措,区分经过不同的线程池来处置。拉取线程池担任拉取信息,消费线程池担任消费信息。

伪代码相似:

如图,在拉取线程外部,咱们拉取完信息后,将信息提交到消费线程 consumeExecutor 。

这样方式可以经过多线程口头大幅度优化消费速度 ,然而这里还是有一个疑问:

假设消费速度很慢,消费者速度很高,那么就会在线程池内容易发生信息沉积,这外面会发生两个隐形危险:

那么如何优化这种形式呢 ?

答案是:拉取线程提交信息到线程池时,当队列中信息数量抵达必定数量时,提交信息到线程池会阻塞。

三、拉取线程 + 消费线程池(阻塞形式)

咱们将信息包装为 Runnable ,而后经过消费线程池口头 execute ,拉取线程会不会阻塞呢 ?

下图是口头的源码:

可以看到,第 30 行调用的是 workQueue 的非阻塞的 offer 方法。

假设队列已满,新提交的义务并不会被 block 住,反而会调用后续的 reject 流程。

假设咱们想要到达阻塞消费者的目的的话,可以采取如下的两种打算:

四、拉取线程 + Disruptor

下图展现了 Disruptor 的流程图 。

和线程池机制十分相似, Disruptor 也是十分典型的消费者/消费者形式。线程池存储提交义务的容器是阻塞队列,而 Disruptor 经常使用的是环形缓冲区 RingBuffer。

环形缓冲区的设计相比阻塞队列有如下好处:

为了防止渣滓回收,驳回数组而非链表。同时,数组对处置器的缓存机制愈加友好。

数组长度 2^n,经过位运算,放慢定位的速度。下标采取递增的方式,不用担忧 index 溢出的疑问。index 是 long 类型,即使100万QPS的处置速度,也须要30万年能力用完。

每个消费者或许消费者线程,会先放开可以操作的元素在数组中的位置,放开到之后,间接在该位置写入或许读取数据。

此刻大家并不须要了解环形缓冲区的读写机制,只要要明确 环形缓冲区 RingBuffer 是 Disruptor 的精髓即可。

将消费线程池交流成 Disruptor 有两个显著的好处:

伪代码相似:

1.定义 Disruptor

2.拉取线程将信息发送到 Disruptor Ringbuffer

3.消费信息

全体的消费者线程模型如下图:

五、平滑停服 + 定时义务补救

当咱们剖析消费者线程模型时,无论咱们经常使用哪种方式,假设主机突然宕机、或许物理机断电,则会失落信息。

笔者介绍两种方式:

1.平滑停服

平滑停服是指在中止运行程序时,尽量防止终止正在启动的恳求或义务,尽量让正在启动的义务处置成功,并且不再接纳新的义务,等一切义务口头成功后封锁运行。

在 Unix/Linux 系统中,可以经常使用 kill 命令发送信号给运转中的进程。

经常出现的信号有:

为了成功平滑停服,可以经常使用 Java 的 Runtime.getRuntime().addShutdownHook 方法注册一个封锁钩子(shutdown hook)。当 JVM 接纳到SIGTERM信号时,封锁钩子会被口头,从而可以在运行程序中止前口头一些清算上班。

Runtime.getRuntime().addShutdownHook(new Thread(() -> {System.out.println("Shutdown hook triggered. Performing cleanup...");// 在这里口头清算上班,如封锁资源、保留形态等}));

咱们可以在钩子里,封锁拉取线程池 ,优雅封锁消费线程池等 ,这样可以尽量防止失落信息。

2.定时义务补救

经常使用 List 做信息队列,无法防止的会有信息失落,所以咱们须要用定时义务做补救,每隔一段期间去业务表里查问业务形态机,若形态机不合乎条件,则触发补救战略。

参考资料:

您可能还会对下面的文章感兴趣: