Redis 列表(List)是一种简单的字符串列表,它的底层实现是一个双向链表。

生产环境,很多公司都将 Redis 列表应用于轻量级消息队列 。这篇文章,我们聊聊如何使用 List 命令实现消息队列的功能以及剖析消费者线程模型 。

1 核心流程

生产者使用 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 表示阻塞等待时间无限制 。

如图,我们启动一个消费线程永动机,消费线程拉取消息后,执行消费逻辑。

这种消费者线程模型非常容易理解,同时也非常适合顺序消费的模式。同时,假如我们在消费消息时,服务器宕机或者断电,可能丢失一条消息。

接下来,我们想一想,有没有消费速度更高的消费模型吗? 笔者根据过往的经历,列举三种模式:

  • 拉取线程 + 消费线程池(非阻塞模式)
  • 拉取线程 + 消费线程池 (阻塞模式)
  • 拉取线程 + Disruptor(阻塞模式)

2 拉取线程 + 消费线程池(非阻塞模式)

为了提升消费速度,我们可以将拉取和消费拆分成两种动作,分别通过不同的线程池来处理。拉取线程池负责拉取消息,消费线程池负责消费消息。

伪代码类似:

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

这样方式可以通过多线程执行大幅度提升消费速度 ,但是这里还是有一个问题:

假如消费速度很慢,生产者速度很高,那么就会在线程池内容易产生消息堆积,这里面会产生两个隐形风险:

  • 线程池队列无限堆积,则可能有 OOM 的风险 ;
  • 假如消费者服务器宕机或者断电,那么会丢失大量的消息。

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

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

3 拉取线程 + 消费线程池(阻塞模式)

我们将消息包装为 Runnable ,然后通过消费线程池执行 execute ,拉取线程会不会阻塞呢 ?

下图是执行的源码:

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

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

如果我们想要达到阻塞生产者的目的的话,可以采取如下的两种方案:

  • 信号量限制同时进入线程池等待队列的任务数 。

  • 使用线程池的拒绝机制,把新加入的任务 put 到等待队列里,这样也可以阻塞住生产者。

4 拉取线程 + Disruptor

下图展示了 Disruptor 的流程图 。

和线程池机制非常类似, Disruptor 也是非常典型的生产者/消费者模式。线程池存储提交任务的容器是阻塞队列,而 Disruptor 使用的是环形缓冲区 RingBuffer。

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

  • 环形数组结构

为了避免垃圾回收,采用数组而非链表。同时,数组对处理器的缓存机制更加友好。

  • 元素位置定位

数组长度 2^n,通过位运算,加快定位的速度。下标采取递增的形式,不用担心 index 溢出的问题。index 是 long 类型,即使100万QPS的处理速度,也需要30万年才能用完。

  • 无锁设计

每个生产者或者消费者线程,会先申请可以操作的元素在数组中的位置,申请到之后,直接在该位置写入或者读取数据。

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

将消费线程池替换成 Disruptor 有两个明显的优点:

  • 无锁队列,写入读取性能非常好
  • 当拉取线程提交消息到 Disruptor 时,若环形缓冲区 RingBuffer 已经满了,则拉取线程会阻塞,这样天然的可以避免无限拉取,同时避免 OOM 的问题。

伪代码类似:

1、定义 Disruptor

2、拉取线程将消息发送到 Disruptor Ringbuffer

3、消费消息

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

5 平滑停服 + 定时任务补偿

当我们分析消费者线程模型时,无论我们使用哪种方式,假如服务器突然宕机、或者物理机断电,则会丢失消息。

笔者推荐两种方式:

1、平滑停服

平滑停服是指在停止应用程序时,尽量避免中断正在进行的请求或任务,尽量让正在进行的任务处理完成,并且不再接收新的任务,等所有任务执行完成后关闭应用。

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

常见的信号有:

  • SIGTERM (15):请求进程终止,可以被捕捉和处理,用于优雅地停止进程。
  • SIGKILL (9):强制终止进程,不能被捕捉或忽略。
  • SIGQUIT (3):进程退出并生成核心转储(core dump)。

为了实现平滑停服,可以使用 Java 的 Runtime.getRuntime().addShutdownHook 方法注册一个关闭钩子(shutdown hook)。当 JVM 接收到SIGTERM信号时,关闭钩子会被执行,从而可以在应用程序停止前执行一些清理工作。

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Shutdown hook triggered. Performing cleanup...");
// 在这里执行清理工作,如关闭资源、保存状态等
}));

我们可以在钩子里,关闭拉取线程池 ,优雅关闭消费线程池等 ,这样可以尽量避免丢失消息。

2、定时任务补偿

使用 List 做消息队列,不可避免的会有消息丢失,所以我们需要用定时任务做补偿,每隔一段时间去业务表里查询业务状态机,若状态机不符合条件,则触发补偿策略。

 

剖析 Redis List 消息队列的三种消费线程模型的更多相关文章

  1. 【ActiveMQ】持久化消息队列的三种方式

    1.ActiveMQ消息持久化方式,分别是:文件.mysql数据库.oracle数据库 2.修改方式: a.文件持久化: ActiveMQ默认的消息保存方式,一般如果没有修改过其他持久化方式的话可以不 ...

  2. Redis作为消息队列服务场景应用案例

    NoSQL初探之人人都爱Redis:(3)使用Redis作为消息队列服务场景应用案例   一.消息队列场景简介 “消息”是在两台计算机间传送的数据单位.消息可以非常简单,例如只包含文本字符串:也可以更 ...

  3. Spring Data Redis实现消息队列——发布/订阅模式

    一般来说,消息队列有两种场景,一种是发布者订阅者模式,一种是生产者消费者模式.利用redis这两种场景的消息队列都能够实现. 定义:生产者消费者模式:生产者生产消息放到队列里,多个消费者同时监听队列, ...

  4. Redis 做消息队列

    一般来说,消息队列有两种场景,一种是发布者订阅者模式,一种是生产者消费者模式.利用redis这两种场景的消息队列都能够实现.定义: 生产者消费者模式:生产者生产消息放到队列里,多个消费者同时监听队列, ...

  5. Lumen开发:结合Redis实现消息队列(1)

    1.简介 Lumen队列服务为各种不同的后台队列提供了统一的API.队列允许你推迟耗时任务(例如发送邮件)的执行,从而大幅提高web请求速度. 1.1 配置 .env文件的QUEUE_DRIVER选项 ...

  6. 程序员过关斩将--redis做消息队列,香吗?

    Redis消息队列 在程序员这个圈子打拼了太多年,见过太多的程序员使用redis,其中一部分喜欢把redis做缓存(cache)使用,其中最典型的当属存储用户session,除此之外,把redis作为 ...

  7. Python队列的三种队列方法

    今天讲一下队列,用到一个python自带的库,queue 队列的三种方法有: 1.FIFO先入先出队列(Queue) 2.LIFO后入先出队列(LifoQueue) 3.优先级队列(PriorityQ ...

  8. Redis 事务 & 消息队列

    Redis 消息队列介绍 什么是消息队列 消息队列(Message Queue)是一种应用间的通信方式,消息发送后可以立即返回,有消息系统来确保信息的可靠传递,消息生产者只管把消息发布到消息队列中而不 ...

  9. redis resque消息队列

    Resque 目前正在学习使用resque .resque-scheduler来发布异步任务和定时任务,为了方便以后查阅,所以记录一下. resque和resque-scheduler其优点在于功能比 ...

  10. 【springboot】【redis】springboot+redis实现发布订阅功能,实现redis的消息队列的功能

    springboot+redis实现发布订阅功能,实现redis的消息队列的功能 参考:https://www.cnblogs.com/cx987514451/p/9529611.html 思考一个问 ...

随机推荐

  1. Vue 3 后端错误消息处理范例

    1. 错误消息格式 前后端消息传递时,我们可以通过 json 的 errors 字段传递错误信息,一个比较好的格式范例为: { errors: { global: ["网络错误"] ...

  2. 说说XXLJob分片任务实现原理?

    XXL Job 是一个开源的分布式任务调度平台,其核心设计目标是开发迅速.学习简单.轻量级.易扩展的分布式任务调度框架. 这两天咱们开发的 AI Cloud 项目中,也使用到了 XXL Job 来执行 ...

  3. oeasy教您玩转python - 4 - # 调试程序

    ​ 调试程序 回忆上次内容 py 的程序都是写在明面上的 所有需要执行的事情都明着写到了 py 文件中 用 python3 解释 py 文件进行执行 可以下载人家写好的 py 文件 下载的 py 文件 ...

  4. 「比赛记录」CF Round 954 (Div. 3)

    Codeforces Round 954 (Div. 3) 题目列表: A. X Axis B. Matrix Stabilization C. Update Queries D. Mathemati ...

  5. Docker 使用Docker创建MySQL容器

    使用Docker创建MySQL容器 实践环境 Docker version 20.10.5 MySQL5.7 Centos 7.8 创建步骤 1.拉取MySQL镜像 docker pull mysql ...

  6. linux部署Python UI自动化项目过程

    1.安装chrome浏览器 下载 访问谷歌中文网站:Google Chrome 网络浏览器. 将页面滑到最下面,点击其他平台, 在弹出的页面选择linux 选择对应的系统版本进行下载. 下载后的deb ...

  7. Python将信息发送到指定邮箱

    目的:将Python执行脚本结果发送到指定邮箱 使用场景:可将每天.每周定时任务python跑的结果汇总,定时发送到小组成员/领导邮箱中 1.以下163邮箱为例,设置发件人是163邮箱,接收人是qq邮 ...

  8. 【JS】03 BOM 浏览器对象模型

    BOM :Broswer Object Model 浏览器对象模型 核心对象是window对象,window对象又可以操作以下的常见对象: - frames[] 窗口对象数组? 浏览器可以打开多个窗口 ...

  9. 【Layui】05 选项卡 Tabs

    文档位置: https://www.layui.com/doc/element/tab.html 案例演示: <div class="layui-tab"> <u ...

  10. 【TypeScript】01 基础入门

    前提:使用TypeScript你需要安装NodeJS支持 然后安装TypeScript: npm intsall -g typescript 安装完成后查看版本号: tsc -v 新建一个TypeSc ...