持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第14天,点击查看活动详情
集成RocketMQ消息生产者
1. 创建服务消息生产者子模块
创建service-provider-rocketmq子模块:
2. 添加RocketMQ依赖
这里可以添加rocketmq-stream依赖,这种方式是基于驱动的消息传递方式,更优;但我们先快速集成RocketMQ,后面再整合这种方式。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-alibaba-starter</artifactId>
<groupId>com.deepinsea</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>service-provider-rocketmq</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- nacos 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- rocketmq 消息队列 -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!-- lombok 构造器代码生成器-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
3. 启动类服务发现声明
编写主启动类src/main/java/com/deepinsea/ServiceProviderRocketMQApplication.java:
package deepinsea;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* Created by deepinsea on 2022/7/29.
* 消息生产者主启动类
*/
@SpringBootApplication
@EnableDiscoveryClient
public class ServiceProviderRocketMQApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceProviderRocketMQApplication.class, args);
}
}
4. 编写消息生产者配置文件
server:
# 服务运行端口
port: 9090
spring:
application:
# 服务名称
name: service-provider-rocketmq
cloud:
nacos:
discovery:
# 服务注册地址
server-addr: localhost:8848
# Nacos认证信息
username: nacos
password: nacos
# 注册到 nacos 的指定 namespace,默认为 public
namespace: public
rocketmq:
name-server: localhost:9876
producer:
# 必须指定group,否则会启动失败
group: rocketmq-group
注意:一定要添加rocketmq.producer.group配置参数,否则会启动失败,报以下错误:
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2022-07-30 01:22:53.293 ERROR 4272 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
A component required a bean of type 'org.apache.rocketmq.spring.core.RocketMQTemplate' that could not be found.
Action:
Consider defining a bean of type 'org.apache.rocketmq.spring.core.RocketMQTemplate' in your configuration.
Process finished with exit code 1
5. 创建消息体实体类
创建一个消息实体类,用于映射消息生产者投递的消息体内容:
package deepinsea.model;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* Created by deepinsea on 2022/7/29.
* 消息体
*/
@Data
public class Message implements Serializable {
private Integer id;
private String name;
private String status;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
}
注意:这里的日期类型字段需要使用Jackson的@JsonFormat注解进行序列化处理,否则会出现与本机时间偏差几小时的问题。
不进行日期字段序列化处理的消息体,如下图所示:
可以看到,消息体中的时间比StoreTime慢8小时,正是默认时区慢8小时造成的。
6. 创建消息生产Controller
package deepinsea.controller;
import deepinsea.model.Message;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Date;
/**
* Created by deepinsea on 2022/7/30.
* 消息生产者控制类
*/
@RestController
@RequestMapping("/provider-rocketmq")
public class RocketMQProducerController {
/**
* 用于发送消息到RocketMQ Broker的API
*/
@Resource
public RocketMQTemplate rocketMQTemplate;
// 模板消息发送
@GetMapping("/sendMsg")
public String sendMsg() {
String topic = "test-topic";
Message message = new Message();
message.setId(1);
message.setName("白羊");
message.setStatus("default");
message.setCreateTime(new Date());
// 发送消息
rocketMQTemplate.convertAndSend(topic, message);
return "send message success";
}
// 实时消息发送
@GetMapping("/send")
public String send(String topic, String msg){
rocketMQTemplate.convertAndSend(topic, msg);
return "send live message success";
}
}
事实上,我在上面的消息生产者中添加两个接口:一个是模板消息发送接口,另一个是实时消息发送接口。生产中,这两种方式一般是结合使用,下面我们分别测试这两种消息发送方式。
7. 测试消息生产接口
模板消息投递测试
我们点击"▶️"启动项目以后,可以使用curl命令访问消息发送的接口:
C:\Users\deepinsea>curl http://localhost:9090/provider-rocketmq/sendMsg
send message success
因为RocketMQ的消息Producer(生产者)客户端发送的消息,将会存储在RocketMQ Broker中,因此我们可以通过RocketMQ Dashboard查看历史消息记录:
注意:一定要点击搜索才会显示消息记录,不点击搜索的话切换到对应的topic也不会去查询消息投递记录的
我们可以点击到RocketMQ控制台的消息选项,然后切换到自定义的test-topic订阅主题,点击搜索后出现对应的消息记录。我们可以点击消息详情查看到具体的消息体(这里我们查看一个序列化消息体日期字段后的消息详细):
可以看到,消息体信息与预期投递的消息结果一致,并且日期也没有时区错位,模板消息投递测试成功!
实时消息投递测试
我们可以使用PostMan请求接口,来测试实时消息的投递:
http://localhost:9090/provider-rocketmq/send?topic=test-topic&msg=hello
可以看到,实时消息投递接口请求成功!
我们还可以使用更轻量级的curl,来进行实时消息投递接口测试:
C:\Users\deepinsea>curl "http://localhost:9090/provider-rocketmq/send?topic=test-topic&msg=hello"
send live message success
同样的,使用curl进行实时消息投递接口请求也是成功的!
注意:这里测试实时消息投递时,添加的消息参数时text/html的,但是Jackson/FastJson序列化的格式为application/json的,因此消费者监听并消费Broker中存储的消息时会因为格式不正确出现反序列错误的问题。因此消息生产者和消费者,最好都采用标准Json的格式传输消息数据,否则就需要通过兼容不同格式的消息数据来解决反序列化的问题了。
我们打开RocketMQ控制台,可以看到Broker中存储了投递的实时消息:
我们打开同一个topic下,通过实时消息投递接口投递的消息详情:
可以看到,消息体内容符合预期结果。这说明只要是同一个Topic主题下的消息,都会放在一起进行存储,类似于RabbitMQ的主题交换机;RocketMQ将其定义为主题订阅的概念,只要订阅了同一个主题的消息都会有相同的投递路径。
RocketMQ消息删除方面的扩展
我们发现生产者已投递到Broker但没有被消费的消息,我们不能直接删除。只能通过停止RocketMQ进程,然后再删除日志和自定义的topic信息,重启RocketMQ后才能成功删除生产的消息。不然的话,只能等待3天后,RocketMQ自动删除存储到Broker的topic消息。
消息积压问题
这种没有被消息消费者消费的消息,积累太多之后会出现消息堆积问题。
解决方案
为了避免生产环境流量暴增引起的短时间消息堆积,有如下解决方案:
- 建议业务系统加强消费能力,不要让消息堆积,消息文件不被占用就可以更安全的被删除;
- fileReservedTime设置合理的时间,保持可用磁盘空间在一定程度,可以挡住短时间的流量冲击;
- 发生堆积时优先删除同一个磁盘空间的其它无用日志;
- 找到消息量大的topic,进行精准删除;
- 动态扩容broker;
- update消息量大的topic的broker。
那么我们投递到Broker的消息被消费之后,会被清理掉吗? 答案是:不会。
原理
消息是被顺序存储在commitlog文件的,且消息大小不定长,所以消息的清理是不可能以消息为单位进行清理的,而是以commitlog文件为单位进行清理的。否则会急剧下降清理效率,并实现逻辑复杂。
commitlog文件存在一个过期时间,默认为72小时,即三天。除了用户手动清理外,在以下情况下也 会被自动清理,无论文件中的消息是否被消费过:
- 文件过期,且到达清理时间点(默认为凌晨4点)后,自动清理过期文件
- 文件过期,且磁盘空间占用率已达过期清理警戒线(默认75%)后,无论是否达到清理时间点, 都会自动清理过期文件
- 磁盘占用率达到清理警戒线(默认85%)后,开始按照设定好的规则清理文件,无论是否过期。 默认会从最老的文件开始清理
- 磁盘占用率达到系统危险警戒线(默认90%)后,Broker将拒绝消息写入
需要注意以下几点:
- 对于RocketMQ来说,删除一个1G大小的文件,是一个压力巨大的IO操作。在删除过程中,系统性能会骤然下降。所以,其默认清理时间点为凌晨4点,访问量最小的时间。也正因如果,我们要保障磁盘空间的空闲率,不要使系统出现在其它时间点删除commitlog文件的情况。
- 官方建议RocketMQ服务的Linux文件系统采用ext4。因为对于文件删除操作,ext4要比ext3性能更好
实际测试
测试消费过的消息不会被删除
- 使用Topic为test-topic,设置生产者往RocketMQ中以test-topic作为Topic发送10条测试消息;
- 消费者监听Topic为test-topic,且Consumer Group为test-consumer-a,那么消费者为test-consumer-a的会消费掉10条消息;
- 此时我们可以在启动一个消费者,这时我们这个新的消费者的Consumer Group不再是test-consumer-a,而是test-consumer-new;
- 那么这个test-consumer-new启动后也会拿到这10条消息。
重置消费位点
RocketMQ中有两种消费模式:
- 集群模式:消费者为同一个组下的,订阅的是同一个topic和tag的情况
这样的就是消费者组成了一个集群,有多个实例,之后就是topic中的消息只会被这些实例中的其中一个消费;
- 广播模式:多个不同组的消费者订阅同一个topic和tag的情况。
这样的就是所谓的广播模式,每个消费者都会接收到topic中的消息。
两种消费模式适用场景以及注意事项:
负载均衡(集群)模式:适用场景&注意事项
- 消费端集群化部署,每条消息只需要被处理一次。
- 由于消费进度在服务端维护,可靠性更高。
- 集群消费模式下,每一条消息都只会被分发到一台机器上处理。如果需要被集群下的每一台机器都处理,请使用广播模式。
- 集群消费模式下,不保证每一次失败重投的消息路由到同一台机器上,因此处理消息时不应该做任何确定性假设。
广播模式:适用场景&注意事项
- 每条消息都需要被相同逻辑的多台机器处理。
- 消费进度在客户端维护,出现重复的概率稍大于集群模式。
- 广播模式下,消息队列 RocketMQ 保证每条消息至少被每台客户端消费一次,但是并不会对消费失败的消息进行失败重投,因此业务方需要关注消费失败的情况。
- 广播模式下,客户端每一次重启都会从最新消息消费。客户端在被停止期间发送至服务端的消息将会被自动跳过,请谨慎选择。
- 广播模式下,每条消息都会被大量的客户端重复处理,因此推荐尽可能使用集群模式。
- 目前仅 Java 客户端支持广播模式。
- 广播消费模式下不支持顺序消息。
- 广播消费模式下不支持重置消费位点。
- 广播模式下服务端不维护消费进度,所以消息队列 RocketMQ 控制台不支持消息堆积查询、消息堆积报警和订阅关系查询功能。
RocketMQ中的消息被消费了并不会被删除(保存3天后滚动删除,一般都需要持久化),消息被消费只是订阅了这个topic的消费者的指针的移动。
虽然我们无法正常删除生产了但没有被消费的消息,但是按照阿里云官方文档,我们可以重置消费位点来删除堆积的消息。
由于消费消息也只是对应的消费者的指针的偏移,消息也并没有消失,所以这个重置消费位点也就是指针的来回移动偏移而已。
而指针的偏移值offset并不能精确到其中的一条消息,只能通过时间来定位一个范围区间内的消息。底层是通过ResetOffsetByTimeCommand.execute() 来实现的。
下面是RocketMQ控制台的重置位点功能(需要对应同一个Topic下的同一个Group):
可通过重置消费位点,按需清除堆积的或不想消费的这部分消息再开始消费,或直接跳转到某个时间点消费该时间点之后的消息(不论是否消费过该时间点之前的消息)。
使用重置消费位点功能有以下注意事项:
- 广播消费模式不支持重置消费位点。
- 目前不支持指定 Message ID、Message Key 和 Tag 来重置消息的消费位点。
按时间点进行消费位点重置:选择该选项后会出现时间点选择的控件,然后选择一个时间点,这个时间点之后发送的消息才会被消费。可选时间范围中的起始和终止时间分别是该 Topic 中储存的最早的和最晚的一条消息的生产时间,不能选择超过可选时间范围的时间点。
还有一点 最重要的一点,这个重置消费位点你点过重置后它不会立马重置,你的消费者也不会立马接收到消息,点过重置后,好像会导致消息短暂的失效(大概是0-3分钟)
消息删除部分的扩展就先到这里,正常请求我们是有默认的消息清理策略的,如果需要优化的话才需要进行调整。我们先进行正常的消息消费,下面集成RocketMQ消息消费者客户端:
集成RocketMQ消息消费者
1. 创建服务消息消费者子模块
2. 添加RocketMQ依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-alibaba-starter</artifactId>
<groupId>com.deepinsea</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>service-consumer-rocketmq</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- nacos 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- rocketmq 消息队列 -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!-- lombok 构造器代码生成器-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
3. 启动类服务发现声明
编写主启动类src/main/java/com/deepinsea/ServiceConsumerRocketMQApplication.java:
package deepinsea;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* Created by deepinsea on 2022/7/30.
* 消息消费者主启动类
*/
@SpringBootApplication
@EnableDiscoveryClient
public class ServiceConsumerRocketMQApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceConsumerRocketMQApplication.class, args);
}
}
4. 编写消息消费者配置文件
server:
# 服务运行端口
port: 10000
spring:
application:
# 服务名称
name: service-consumer-rocketmq
cloud:
nacos:
discovery:
# 服务注册地址
server-addr: localhost:8848
# Nacos认证信息
username: nacos
password: nacos
# 注册到 nacos 的指定 namespace,默认为 public
namespace: public
rocketmq:
name-server: localhost:9876
producer:
# 必须指定group,否则会启动失败
group: rocketmq-group
5. 创建消息消费者监听器
这里需要指定MQ监听器的topic和group name,对应两大消息消费模式中的订阅模式。下面是阿里云文档中的说明:
订阅关系一致:指的是同一个消费者Group ID下所有Consumer实例所订阅的Topic、Tag必须完全一致。如果订阅关系不一致,消息消费的逻辑就会混乱,甚至导致消息丢失。
因此,可以了解到:Topic订阅关系路由范围 > Group ID。
这里需要注意一下,消息反序列化类型错误的问题
package deepinsea.common.listener;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Created by deepinsea on 2022/7/30.
* 消息消费者监听器
*/
@Component
@RocketMQMessageListener(topic = "test-topic", consumerGroup = "rocketmq-group")
// topic需要和生产者的topic一致,consumerGroup属性是必须指定的,内容可以随意
//public class RocketMQConsumerListener implements RocketMQListener<Message> {
public class RocketMQConsumerListener implements RocketMQListener<String> {
//public class RocketMQConsumerListener implements RocketMQListener<Object> {
private static final Logger log = LoggerFactory.getLogger(RocketMQConsumerListener.class);
/**
* 监听到消息就会调用该方法
*/
@Override
// public void onMessage(Message message) {
public void onMessage(String str) {
// public void onMessage(Object obj) {
// log.info("从test-topic订阅主题中监听到消息");
// log.info(JSON.toJSONString(message)); //json对象转为json字符串(一般推荐是json字符串传输,但大部分时候都是对象直接传输)
log.info("从test-topic订阅主题中监听到消息: ");
log.info(str);
// log.info("从test-topic订阅主题中监听到消息");
// log.info(obj);
}
}
注意:这里监听类重写的消息接收方法,接收的参数类型最好为String,不然会出现Json数据反序列化异常的问题。另外,也不推荐使用Object类来兼容所有类型的传输数据,因为Object作为接收参数会导致频繁装箱拆箱,造成性能问题。
如果定义三个监听器,分别采用Message、String和Object作为接收参数类型,那么只有Object类型的监听器会生效。如果没有Object类型的监听器,只有其他两种监听器时,Message类型的监听器会不兼容格式的反序列化错误。因此,这种分别接收的方式不推荐。
因为application/json和text/html这两种类型的数据,都是字符串,因此采用String类型接收可以兼容。
另外如果需要统一模板消息和实时消息数据传输的格式,最好是消息生产传输时设置一个key,将text/html文本字符串设置为key的值,将key-value转为application/json的格式。最好是有一个消息发送平台,可以定义消息模板、输入消息内容、批量推送消息。
6. 测试消息消费者消费消息
我们下面使用curl命令分别生产模板和实时两种消息,然后测试消息消费者对两种消息的接收(消费)情况:
先测试消息生产者,不对实时消息序列化处理的情况
C:\Users\deepinsea>curl http://localhost:9090/provider-rocketmq/sendMsg
send message success
C:\Users\deepinsea>curl "http://localhost:9090/provider-rocketmq/send?topic=test-topic&msg=hello123"
send live message success
控制台输出如下:
2022-07-31 05:53:26.218 INFO 54416 --- [ocketmq-group_9] d.c.listener.RocketMQConsumerListener : 从test-topic订阅主题中监听到消息:
2022-07-31 05:53:26.218 INFO 54416 --- [ocketmq-group_9] d.c.listener.RocketMQConsumerListener : {"id":1,"name":"白羊","status":"default","createTime":"2022-07-31 05:53:26"}
2022-07-31 05:53:28.527 INFO 54416 --- [cketmq-group_10] d.c.listener.RocketMQConsumerListener : 从test-topic订阅主题中监听到消息:
2022-07-31 05:53:28.528 INFO 54416 --- [cketmq-group_10] d.c.listener.RocketMQConsumerListener : hello123
可以看到,消息消费成功!并且,使用String类型接收消息体,兼容application/json和text/html这两种类型的Json字符串格式。
欢迎点赞,谢谢各位大佬了ヾ(◍°∇°◍)ノ゙