常用消息队列简介以及Redis 消息队列的实现三种方案(List、Pub/Sub、Stream)
1、消息队列
消息队列(Messeage Queue,MQ)是在分布式系统架构中常用的一种中间件技术,从字面表述看,是一个存储消息的队列,所以它一般用于给 MQ 中间的两个组件提供通信服务。
1.1 消息队列介绍
我们引入一个削峰填谷实际场景来介绍 MQ ,削峰填谷是指处理短时间内爆发的请求任务,将巨量请求任务“削峰”,平摊在平常请求任务较低的时间段,也就是“填谷”。 比如组件1 发布请求任务,组件2接受请求任务并处理。如果没有 MQ , 组件2 就会在大量的请求任务下会出现假死的情况:
而如果使用 MQ 后可以将这些请求先暂存到队列中,排队执行,就不会出现组件2 假死的情况了。我们一般把发送消息的组件称为生产者,接受消息的组件称为消费者,如下图展示一个消息队列的模型:
消息队列需要满足消息有序性、能处理重复的消息以及消息可靠性,这样才能保证存取消息的一致性。
消息有序性:虽然消费者异步读取消息,但是要按照生产者发送消息的顺序来处理消息,避免后发送的消息被先处理掉。
重复消息处理:在消息队列存取信息时,有可能因为网络阻塞而出现消息重传的情况。可能会造成业务逻辑被多次执行,所以要避免重复消息的处理。
消息可靠性:在组件故障时,比如消费者宕机或者没有处理完信息时,消息队列需要能提供消息可靠性保证。所以需要在消费者故障时,可以重新读取消息再次进行处理,不影响业务服务。
1.2 消息队列应用场景
主要的应用有:异步处理、流量削峰、系统解耦
1.2.1 商品秒杀
秒杀活动中,会短时间出现爆发式的用户请求,如果没有消息队列,会导致服务器响应不过来。轻则会导致服务假死;重则会让服务器直接宕机。
这时可以加上消息队列,服务器接收到用户的请求后,先把这些请求全部写入消息队列中再排队处理,这样就不会导致同时处理多个请求的情况;若消息队列长度超过承载的最大数量,可以抛弃后续的消息,给用户返回“页面出错,请重新刷新”提示,这样降低服务器的负载,而且也能给用户很好的交互体验。
1.2.2 系统解耦
此外,我们可以利用消息队列来把系统的业务功能模块化,实现系统功能的解耦。如下图:
如果有两个功能服务,而且关系不是很紧密,比如订单系统和优惠券,虽然都和用户有关联,但是如果都放在用户模块,面临功能删减时会很麻烦。所以采用把两个服务独立出来,而将两个服务的消息发送以约定的方式通过消息队列发送过去,让其对应的消费者分别处理即可达到系统解耦的目的。
1.3 常见的消息队列中间件
1.3.1 RabbitMQ
1.3.1.1 RabbitMQ 介绍
RabbitMQ 是一个老牌的开源消息中间件,它实现了标准的 AMQP(Advanced Message Queuing Protocol,高级消息队列协议)消息中间件,使用 Erlang 语言开发,支持集群部署。支持 java、python、Go、.NET 等等主流开发语言。
其主要的运行流程如下图:
1.3.1.2 RabbitMQ 特点
支持持久化,RabbitMQ 支持磁盘持久化功能,保证了消息不会丢失;
高并发,RabbitMQ 使用了 Erlang 开发语言,Erlang 是为电话交换机开发的语言,天生自带高并发光环和高可用特性;
支持分布式集群,正是因为 Erlang 语言实现的,因此 RabbitMQ 集群部署也非常简单,只需要启动每个节点并使用 --link 把节点加入到集群中即可,并且 RabbitMQ 支持自动选主和自动容灾;
支持多种语言,比如 Java、.NET、PHP、Python、JavaScript、Ruby、Go 等;
支持消息确认,支持消息消费确认(ack)保证了每条消息可以被正常消费;
它支持很多插件,比如网页控制台消息管理插件、消息延迟插件等,RabbitMQ 的插件很多并且使用都很方便。
因为中间中的交换器模块,所以RabbitMQ 有不同的消息类型,主要分为以下几种:
direct(默认类型)模式,此模式为一对一的发送方式,也就是一条消息只会发送给一个消费者;
headers 模式,允许你匹配消息的 header 而非路由键(RoutingKey),除此之外 headers 和 direct 的使用完全一致,但因为 headers 匹配的性能很差,几乎不会被用到;
fanout 模式,为多播的方式,会把一个消息分发给所有的订阅者;
topic 模式,为主题订阅模式,允许使用通配符(#、*)匹配一个或者多个消息,我可以使用“cn.mq.#”匹配到多个前缀是“cn.mq.xxx”的消息,比如可以匹配到“cn.mq.rabbit”、“cn.mq.kafka”等消息。
但是 Rabbit 也存在以下的问题:
RabbitMQ 对消息堆积的支持并不好,当大量消息积压的时候,会导致 RabbitMQ 的性能急剧下降。
RabbitMQ 的性能是这几个消息队列中最差的,大概每秒钟可以处理几万到十几万条消息。如果应用对消息队列的性能要求非常高,那不要选择 RabbitMQ。
RabbitMQ 使用的编程语言 Erlang,扩展和二次开发成本高。
1.3.2 Kafka
1.3.2.1 Kafka 介绍
Kafka 是 LinkedIn 公司开发的基于 ZooKeeper 的多分区、多副本的分布式消息系统,它于 2010 年贡献给了 Apache 基金会,并且成为了 Apache 的顶级开源项目。其中 ZooKeeper 的作用是用来为 Kafka 提供集群元数据管理以及节点的选举和发现等功能。
与 RabbitMQ 不同中间的 Kafka 集群部分是由 Broker 代理和 ZooKeeper 集群组成:
1.3.2.2 Kafka 特点
Kafka 与周边生态系统的兼容性是最好的没有之一,尤其在大数据和流计算领域,几乎所有的相关开源软件系统都会优先支持 Kafka。
Kafka 性能高效、可扩展良好并且可持久化。它的分区特性,可复制和可容错都是不错的特性。
Kafka 使用 Scala 和 Java 语言开发,设计上大量使用了批量和异步的思想,使得 Kafka 能做到超高的性能。Kafka 的性能,尤其是异步收发的性能,是三者中最好的,但与 RocketMQ 并没有量级上的差异,大约每秒钟可以处理几十万条消息。
在有足够的客户端并发进行异步批量发送,并且开启压缩的情况下,Kafka 的极限处理能力可以超过每秒 2000 万条消息。
同时 Kafka 也有缺点:
Kafka 同步收发消息的响应时延较高。因为其异步批量的设计带来的问题,在它的 Broker 中,很多地方都会使用这种先攒一波再一起处理的设计。当你的业务场景中,每秒钟消息数量没有那么多的时候,Kafka 的时延反而会比较高。所以,Kafka 不太适合在线业务场景。
1.3.3 RocketMQ
1.3.3.1 RocketMQ 介绍
RocketMQ 是阿里巴巴开源的分布式消息中间件,用 Java 语言实现,在设计时参考了 Kafka,并做出了自己的一些改进,后来捐赠给 Apache 软件基金会。支持事务消息、顺序消息、批量消息、定时消息、消息回溯等。它里面有几个区别于标准消息中件间的概念,如Group、Topic、Queue等。系统组成则由Producer、Consumer、Broker、NameServer等
RocketMQ 要求生产者和消费者必须是一个集群。集群级别的高可用,是RocketMQ 和其他 MQ 的区别。
Name Server(名称服务提供者) :是一个几乎无状态节点,可集群部署,节点之间没有任何信息同步。提供命令、更新和发现 Broker 服务
Broker (消息中转提供者):负责存储转发消息
broker分为 Master Broker 和 Slave Broker,一个 Master Broker 可以对应多个 Slave Broker,但是一个 Slave Broker 只能对应一个 Master Broker。
1.3.3.2 RocketMQ 特点
是一个队列模型的消息中间件,具有高性能、高可靠、高实时、分布式等特点
Producer 向一些队列轮流发送消息,队列集合称为 Topic,Consumer 如果做广播消费,则一个 Consumer 实例消费这个 Topic 对应的所有队列,如果做集群消费,则多个 Consumer 实例平均消费这个 Topic 对应的队列集合
RocketMQ 的性能比 RabbitMQ 要高一个数量级,每秒钟大概能处理几十万条消息
RocketMQ 的劣势是与周边生态系统的集成和兼容程度不够。
2、redis的三种方式实现
2.1 基于List实现消息队列
实现方式
消息队列(Message Queue),字面意思就是存放消息的队列,而Redis的list数据结构是一个双向链表,很容易模拟出队列的效果
队列的入口和出口不在同一边,所以我们可以利用:LPUSH结合RPOP或者RPUSH结合LPOP来实现消息队列。
不过需要注意的是,当队列中没有消息时,RPOP和LPOP操作会返回NULL,而不像JVM阻塞队列那样会阻塞,并等待消息,不停获取空消息会导致 Redis CPU 的空耗,造成资源浪费。可以通过让消费者休眠的方式的方式来处理,但是这样又会又消息的延迟问题。因此使用
使用BRPOP或者BLPOP来实现阻塞效果,BRPOP 是 RPOP 的阻塞版本,list 为空的时候,它会一直阻塞,直到 list 中有值或者超时。
public class RedisMessageQueueDemo {
private static final String QUEUE_NAME = "messageQueue";
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379); // 连接到本地Redis服务器
// 生产者:向队列中添加消息
new Thread(() -> {
for (int i = 1; i <= 5; i++) {
String message = "Message " + i;
jedis.lpush(QUEUE_NAME, message);
System.out.println("Produced: " + message);
try {
Thread.sleep(500); // 模拟生产者间隔
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}).start();
// 消费者:阻塞式从队列中获取并处理消息
new Thread(() -> {
while (true) {
// BRPOP会阻塞直到有消息到达
String message = jedis.brpop(0, QUEUE_NAME).get(1); // 0表示无限等待
System.out.println("Consumed: " + message);
}
}).start();
}
}
基于List的消息队列有哪些优缺点?
使用 Redis 的 List 数据结构实现消息队列有以下优缺点:
优点:
轻量级:Redis 本身是一个内存数据库,使用 List 结构实现消息队列相对简单,易于部署和维护。
简单易用:List 提供了直观的操作命令,如 LPUSH 和 RPUSH 用于添加消息,LPOP 和 RPOP 用于移除消息,使得实现消息队列变得简单直接。
高性能:因为 Redis 是基于内存的,List 操作都是原子的,可以提供低延迟的处理,适合需要快速入队和出队的场景。
支持持久化:Redis 支持持久化机制,可以将消息队列的数据保存到磁盘,保证数据安全性。
支持消息有序性:List 结构保持了消息的插入顺序,可以满足消息有序性的需求。
缺点:
- 无法避免消息丢失:如果在消费者使用 BRPOP 获取数据的过程中出现异常导致返回数据失败,数据可能会丢失,因为数据已经从队列中移除。
- 只支持单消费者:List 结构的消息队列在消费后即从 List 中删除,这意味着数据只能被一个消费者消费,不支持多个消费者同时消费同一批数据。
- 实时消费问题:如果不使用 BLPOP 或 BRPOP 这样的阻塞命令,消费者需要不断轮询队列,这可能会导致 CPU 资源的浪费。
- 可靠性问题:List 结构本身没有消息确认机制,如果消费者在处理消息时崩溃,消息可能会丢失,需要额外的机制来保证消息的可靠性。
- 消息堆积问题:如果生产者生产消息的速度远大于消费者处理的速度,可能会导致消息大量堆积,占用大量内存资源。
2.2 基于PubSub的消息队列
什么事发布订阅模式?
发布订阅模式是一种消息传递模式,其中发布者发送消息而不直接指定接收者,订阅者通过注册感兴趣的主题来接收消息。当发布者发布某个主题的消息时,所有订阅该主题的消费者都会收到该消息。这种模式有效地实现了解耦,使得生产者和消费者之间的交互更加灵活,常用于事件驱动架构和消息队列系统中。
发布/订阅模式可以 1:N 的消息发布/订阅。发布者将消息发布到指定的频道频道(channel),订阅相应频道的客户端都能收到消息。
PubSub(发布订阅)是Redis2.0版本引入的消息传递模型。
订阅模式匹配规则
订阅客户端到给定的模式。支持的通配符模式包括:
h?flo 订阅 hello、hallo 和 hxllo
h*llo 订阅 hllo 和 heeeello
h[ae]llo 订阅 hello 和 hallo,但不包括 hillo
如果需要逐字匹配特殊字符,可以使用 \ 进行转义。
在使用发布订阅功能时,需要理解两点。
1、保证先让消费者先订阅队列,然后再让生产者发布消息。
如果消费者异常挂掉并重新上线,它只能接收新的消息。在其下线期间,生产者发布的消息将无法被消费者接收,因为找不到该消费者,这些消息会被丢弃。
如果所有消费者都下线,那么生产者发布的消息将因为没有任何消费者而全部被丢弃。
2、发布订阅不具备数据持久化的能力
发布订阅相关的操作不会被写入到 Redis 数据库(RDB)或追加写入文件(AOF)中。如果 Redis 宕机并重新启动,Pub/Sub 的数据将会全部丢失。
基于上面的分析,发布订阅功能特别适合对少量消息丢失不敏感的通知类型的场景。
举例:
例如分布式部署的场景下,将一些信息保存到服务器本地cache提高信息,当这些信息改动时,要刷新所有服务器本地的cache信息,则可以使用发布订阅刷新本地的cache
发布者
redisTemplate.convertAndSend(channelName, msg);
配置redis订阅监听和接受
package com.baidu.ipcp.sourcing.supplier.service.impl;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;
@Component
public class RedisMessageListener implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
String topic = new String(pattern);
if ("test".equals(topic)){
System.out.println(message.toString());
// 后续的操作
}
}
}
package com.baidu.ipcp.sourcing.supplier.service.impl;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
@Configuration
public class IRedisConfig {
/**
* 其他的redis配置.....
*/
/**
* 配置Redis消息监听器容器:,用于注册并管理消息监听器。
*/
RedisMessageListenerContainer container(RedisMessageListener listener){
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
// 订阅test 的topic
container.addMessageListener(listener,new PatternTopic("test"));
return container;
}
}
基于PubSub的消息队列有哪些优缺点
基于 Pub/Sub 的消息队列有以下优缺点:
优点:
实时性:Pub/Sub 模式允许消息的即时传递,订阅者能够快速接收到发布者发送的消息,适合需要快速响应的应用场景,如实时聊天、在线游戏等。
解耦合:发布者和订阅者之间是松耦合的,发布者无需知道订阅者的存在,增强了系统的灵活性和可扩展性。
多频道支持:可以同时订阅多个频道,允许订阅者接收来自不同频道的消息。
缺点:
消息丢失:如果在发布消息时没有任何订阅者在线,消息将被丢弃,无法持久化。这意味着在网络故障或 Redis 宕机时,消息会丢失。
缺乏确认机制:Pub/Sub 模式没有消息确认机制,无法保证消息是否被成功接收和处理。这可能导致消费者在处理消息时出现问题而无法重试。
性能压力:当订阅者数量较多时,发布大量消息可能会对 Redis 服务器造成较大负担,影响系统性能。
2.3 基于Stream的消息队列
Stream是Redis 5.0引入的一种新数据类型,可以时间一个功能非常完善的消息队列。从Spring Data Redis 2.1开始,支持Redis 5.0及以上版本的Stream数据类型。如果你使用的是Spring Boot 2.1以下版本,请考虑升级Spring Boot。
它提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。它就像是个仅追加内容的消息链表,把所有加入的消息都串起来,每个消息都有一个唯一的 ID 和对应的内容。而且消息是持久化的。
发送消息的命令
XADD key [NOMKSTREAM] [MAXLEN|MINID [=!~] threshold [LIMIT count]] *|ID field value [field value ...]
*NOMKSTREAM:如果队列不存在,是否自动创建队列,默认是自动创建
[MAXLEN|MINID [=!~] threshold [LIMIT count]]:设置消息队列的最大消息数量,不设置则无上限
|ID:消息的唯一id,代表由Redis自动生成。格式是”时间戳-递增数字”,例如”114514114514-0”
field value [field value …]:发送到队列中的消息,称为Entry。格式就是多个key-value键值对
举例:
## 创建名为users的队列,并向其中发送一个消息,内容是{name=jack, age=21},并且使用Redis自动生成ID
XADD users * name jack age 21
读取消息的方式之一:XREAD
XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...]
[COUNT count]:每次读取消息的最大数量
[BLOCK milliseconds]:当没有消息时,是否阻塞,阻塞时长
STREAMS key [key …]:要从哪个队列读取消息,key就是队列名
ID [ID …]:起始ID,只返回大于该ID的消息,0表示从第一个消息开始,$表示从最新的消息开始
例如:使用XREAD读取第一个消息
服务器:0>XREAD COUNT 1 STREAMS users 0
1) 1) "users"
2) 1) 1) "1667119621804-0"
2) 1) "name"
2) "jack"
3) "age"
4) "21"
示例: 参考:https://blog.csdn.net/doupengzp/article/details/131221838
引入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
yaml文件配置消费组信息,这里为了可扩展性,写死在代码也可以
redis:
mq:
streams:
# key名称
- name: RARSP:REPORT:READ:VS
groups:
# 消费组名称
- name: VS_GROUP
消费者名称
consumers: VS-CONSUMER-A,VS-CONSUMER-B
# key2
- name: RARSP:REPORT:READ:BLC
groups:
- name: BLC_GROUP
consumers: BLC-CONSUMER-A,BLC-CONSUMER-B
# key3
- name: RARSP:REPORT:READ:HD
groups:
- name: HD_GROUP
consumers: HD-CONSUMER-A,HD-CONSUMER-B
读取配置:定义读取的类
public class RedisMqGroup {
private String name;
private String[] consumers;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String[] getConsumers() {
return consumers;
}
public void setConsumers(String[] consumers) {
this.consumers = consumers;
}
}
public class RedisMqStream {
public String name;
public List<RedisMqGroup> groups;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<RedisMqGroup> getGroups() {
return groups;
}
public void setGroups(List<RedisMqGroup> groups) {
this.groups = groups;
}
}
读取配置
@EnableConfigurationProperties
@Configuration
@ConfigurationProperties(prefix = "redis.mq")
public class RedisMq {
public List<RedisMqStream> streams;
public List<RedisMqStream> getStreams() {
return streams;
}
public void setStreams(List<RedisMqStream> streams) {
this.streams = streams;
}
}
redis序列化配置:这里是平常的redis配置RedisConfig.class
@Configuration
public class BeanConfig {
/**
* RedisTemplate 实例。具体功能如下:
* 创建 RedisTemplate 实例:初始化一个 RedisTemplate 对象,并设置其连接工厂。
* 配置序列化器:
* 使用 Jackson2JsonRedisSerializer 序列化对象值。
* 使用 StringRedisSerializer 序列化字符串键和哈希键。
* 配置 ObjectMapper:设置 ObjectMapper 的可见性和默认类型支持。
* 设置序列化器:将配置好的序列化器应用到 RedisTemplate 中。
* 初始化 RedisTemplate:调用 afterPropertiesSet 方法完成初始化。
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
定义stream用到的redis方法,工具类
@Component
public class RedisStreamUtil {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 创建消费组
* @param key stream-key值
* @param group 消费组
* @return java.lang.String
*/
public String createGroup(String key, String group){
return stringRedisTemplate.opsForStream().createGroup(key, group);
}
/**
* 获取消费者信息
* @param key stream-key值
* @param group 消费组
* @return org.springframework.data.redis.connection.stream.StreamInfo.XInfoConsumers
*/
public StreamInfo.XInfoConsumers queryConsumers(String key, String group){
return stringRedisTemplate.opsForStream().consumers(key, group);
}
/**
* 添加Map消息
* @param key stream对应的key
* @param value 消息数据
* @return
*/
public String addMap(String key, Map<String, Object> value){
return stringRedisTemplate.opsForStream().add(key, value).getValue();
}
/**
* 读取消息
* @param: key
* @return java.util.List<org.springframework.data.redis.connection.stream.MapRecord<java.lang.String,java.lang.Object,java.lang.Object>>
*/
public List<MapRecord<String, Object, Object>> read(String key){
return stringRedisTemplate.opsForStream().read(StreamOffset.fromStart(key));
}
/**
* 确认消费
* @param key
* @param group
* @param recordIds
* @return java.lang.Long
*/
public Long ack(String key, String group, String... recordIds){
return stringRedisTemplate.opsForStream().acknowledge(key, group, recordIds);
}
/**
* 删除消息。当一个节点的所有消息都被删除,那么该节点会自动销毁
* @param: key
* @param: recordIds
* @return java.lang.Long
*/
public Long del(String key, String... recordIds){
return stringRedisTemplate.opsForStream().delete(key, recordIds);
}
/**
* 判断是否存在key
* @param key
* @return
*/
public boolean hasKey(String key){
Boolean aBoolean = stringRedisTemplate.hasKey(key);
return aBoolean==null?false:aBoolean;
}
}
生产者生产
@Autowired
RedisMq redisMq;
@GetMapping("/test/product")
public Result product(){
Map<String,Object> map = new HashMap<>();
map.put("info","生产一个苹果");
// 生产消息,向yaml配置的RARSP:REPORT:READ:VS 发送一个消息
String id = redisUtil.addMap(redisMq.getStreams().get(0).getName(), map);
return Result.success(id);
}
消费者:
定义redis stream流配置类
@Slf4j
@Configuration
public class RedisStreamConfiguration {
@Resource
private RedisTemplate redisTemplate;
@Resource
private RedisStreamUtil redisStreamUtil;
@Resource
private RedisMq redisMq;
/**
* 具体步骤如下:
*
* 配置线程池:根据CPU核心数配置线程池,用于处理消息消费任务。
* 配置监听容器:设置监听容器的批量大小、超时时间、错误处理器等参数。
*
*初始化消费主题和组信息:检查并创建Redis流主题和消费组。
* 创建监听容器:使用配置好的参数创建监听容器。
* 订阅消息:通过监听容器订阅指定主题的消息,并指定消息处理器。
* 启动监听:启动监听容器,开始消费消息。
*
*/
@Bean
public List<Subscription> subscription(RedisConnectionFactory factory){
List<Subscription> resultList = new ArrayList<>();
AtomicInteger index = new AtomicInteger(1);
int processors = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(processors, processors, 0, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(), r -> {
Thread thread = new Thread(r);
thread.setName("async-stream-consumer-" + index.getAndIncrement());
thread.setDaemon(true);
return thread;
});
// 创建选项构建器:初始化 StreamMessageListenerContainerOptions 的构建器,用于 //后续配置。
//设置批量大小:指定每次从Redis流中读取的消息数量为5条,减少频繁的I/O操作。
//设置线程池:指定用于处理消息的线程池,确保消息处理的并发性和性能。
//设置轮询超时时间:指定轮询Redis流的超时时间为1秒,避免长时间等待无消息的情况。
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> options =
StreamMessageListenerContainer
.StreamMessageListenerContainerOptions
.builder()
// 一次最多获取多少条消息
.batchSize(5)
.executor(executor)
.pollTimeout(Duration.ofSeconds(1))
// .errorHandler()
.build();
for (RedisMqStream redisMqStream :redisMq.getStreams()) {
String streamName = redisMqStream.getName();
RedisMqGroup redisMqGroup = redisMqStream.getGroups().get(0);
initStream(streamName,redisMqGroup.getName());
// 创建监听容器
var listenerContainer = StreamMessageListenerContainer.create(factory,options);
// 手动ask消息,注意这里的redisStreamUtil是传值到消费类的,如果用 @Autowired会空指针
Subscription subscription = listenerContainer.receive(Consumer.from(redisMqGroup.getName(), redisMqGroup.getConsumers()[0]),
StreamOffset.create(streamName, ReadOffset.lastConsumed()), new ReportReadMqListener(redisStreamUtil));
// 自动ask消息
/* Subscription subscription = listenerContainer.receiveAutoAck(Consumer.from(redisMqGroup.getName(), redisMqGroup.getConsumers()[0]),
StreamOffset.create(streamName, ReadOffset.lastConsumed()), new ReportReadMqListener());*/
resultList.add(subscription);
listenerContainer.start();
}
return resultList;
}
private void initStream(String key, String group){
boolean hasKey = redisStreamUtil.hasKey(key);
if(!hasKey){
Map<String,Object> map = new HashMap<>(1);
map.put("field","value");
//创建主题
String result = redisStreamUtil.addMap(key, map);
//创建消费组
redisStreamUtil.createGroup(key,group);
//将初始化的值删除掉
redisStreamUtil.del(key,result);
log.info("stream:{}-group:{} initialize success",key,group);
}
}
}
消费类:
@Slf4j
@Component
@RequiredArgsConstructor
public class ReportReadMqListener implements StreamListener<String, MapRecord<String, String, String>> {
public final RedisStreamUtil redisStreamUtil;
@Override
public void onMessage(MapRecord<String, String, String> message) {
// stream的key值
String streamKey = message.getStream();
//消息ID
RecordId recordId = message.getId();
//消息内容
Map<String, String> msg = message.getValue();
//TODO 处理逻辑
//逻辑处理完成后,ack消息,删除消息,group为消费组名称
redisStreamUtil.ack(streamKey,group,recordId.getValue());
redisStreamUtil.del(streamKey,recordId.getValue());
}
}
运行,调用生产者,在控制台出现消费者消费