概述
MQ(Message Queue),也就是 消息队列,就是在消息的传输过程中保存消息的容器。目前比较主流的有 RocketMQ、RabbitMQ、ActiveMQ(比较老了) 以及 Kafka(一个项目可能也会集成多种 MQ 产品来实现不同功能),以下是不同 MQ 的基本信息。
使用 MQ 的好处
服务解耦
典型的消费者-生产者模型:生产者只需要关注发消息,消费者只需要关注收消息,没有业务逻辑的侵入,从而实现解耦。假设业务 A 需要处理订单,又得发短信、邮件,同一代码耦合起来会疯的,那么就只需要发个异步消息让其他专门发短信的业务去做就好了。A 不需要去关心短信的业务逻辑。
流量削峰
假设 A 服务平时访问量是每秒 300 请求,一台服务器可轻松应对 。但在秒杀的时候, 访问量翻了几十倍,达到每秒 30000次 请求,单台服务器就挂了,想着增加多几台服务器减压,可是这种高压每天只出现一次,一次跟你啪啪速度一样快(见鬼了)。那这么多服务器在平时都只分担每秒几十次请求,这样很浪费资源了,这时可以设置一个 MQ 当用户发起请求后台并不会立刻处理,而是通过 MQ 发送一个请求,发送到队列里面,排队等待处理… 然后接收者,发现队列中有消息,一个一个的取出,慢慢进行后台处理…
异步调用
A 服务是给用户外卖下单,订单数据可以发送到消息队列服务器,A 立刻响应客户端 “商家准备中”,
消息接收方 B 监听获取每一个订单消息后,发送订单给商家,接收方 C 监听后寻找快递小哥给商家。
队列主流协议
- AMQP:是一个二进制协议,具有以下特性:多通道、可协商、异步、安全、高效等。
- MQTT:是用于消息队列服务的轻量级、发布订阅、端到端网络协议,运行在TCP/IP协议之上,为资源或网络带宽有限的远程设备提供实时有效的消息服务。
- Kafka:Kafka、RocketMQ都是使用的自定义协议,用于处理海量流式数据,具有分布式、高性能、高可靠、低延迟、高扩展性等特征。
消息传输模型
主流的消息中间件的传输模型主要为点对点模型和发布订阅模型。
点对点模型:也叫队列模型,具有匿名消费(上下游沟通的唯一的身份就是队列,消费者从队列获取消息无法申明独立身份)和一对一通信特点(下游消费者即使有多个,但都没有自己独立的身份,因此共享队列中的消息,每一条消息都只会被唯一的消费者处理。因此点对点模型只能实现一对一通信。)
发布订阅模型:相比队列模型的匿名消费方式,发布订阅模型中消费方都会具备的身份,一般叫做订阅组(订阅关系),不同订阅组之间相互独立不会相互影响。而基于独立身份的设计,同一个主题内的消息可以被多个订阅组处理,每个订阅组都可以拿到全量消息。因此发布订阅模型可以实现一对多通信。
安装
这里主要使用 RocketMQ,安装可以参考 Docker Compose 部署。
因为有些朋友总喜欢捣鼓怎么安装软件,这里真的不建议(纯属浪费时间),对技术无任何提升,我们是使用工具来替我们做事,不是去研究他怎么安装更正确。当然一点配置还是需要懂,但是官网有对应的参数可以参考
概念
简单说,就是 Producer 生成某个 Topic 的 Message ,通过 Queue 发送到 Broker,Consumer 通过订阅 Topic,把 Queue 里面的 Message 拉取过来。
消息模型(Message Model)
RocketMQ 主要由 Producer、Broker、Consumer 三部分组成,Producer 生产消息,Consumer 消费消息,Broker 存储消息。Broker 在实际部署过程中对应一台服务器,每个 Broker 可以存储多个Topic的消息
名字服务(Name Server)与 代理服务器(Broker Server)
-
Name Server 简单说就是给 Broker Server 用的注册中心功能(类似于 Nacos 或者 Zookeeper),充当路由消息的提供者。生产者或消费者能够通过名字服务查找各主题相应的 Broker IP 列表。
-
这里的代理服务器别跟 nginx 什么的弄混了,这里的 Broker Server 是消息中转角色,负责存储消息、转发消息。负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备。 Broker Server 也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等。
消息生产者(Producer)与 生产者组(Producer Group)
-
消息生产者会把业务应用系统里产生的消息发送到 Broker Server。有多种发送方式:同步、异步、顺序、单向。同步和异步方式均需要 Broker 返回确认信息,单向发送不需要。
-
生产者组就是把 同一类的生产者给组合到一块,同一组 Producer 发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,则 Broker 服务器会联系同一生产者组的其他生产者实例以提交或回溯消费。
消息消费者(Consumer)与 消费者组(Consumer Group)
-
消息消费者会从 Broker Server 拉取消息并将其提供给应用程序。从用户应用的角度而言提供了两种消费形式:拉取式消费、推动式消费。
-
消费者组就是同一类的消费者放到一个组里,这类 Consumer 通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的 Topic。RocketMQ 支持集群消费(Clustering)和广播消费(Broadcasting)。
主题(Topic)与 队列(Queue)
-
主题用来隔离不同消息类型的,是某一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是进行消息订阅的基本单位。比如短信有短信的主题和邮件的主题。
-
队列( Topic 分区,称作 Message Queue) 是消息存储和传输的实际容器,也是消息的最小存储单元。队列天然具备顺序性。RocketMQ 的主题都是由多个队列组成,以此实现队列数量的水平拆分和队列内部的流式存储。RocketMQ 队列模型和 Kafka 的分区(Partition)模型类似。
拉取式消费(Pull Consumer)与 推动式消费(Push Consumer)
- 拉取式消费通常主动调用 Consumer 的拉消息方法从 Broker 服务器拉消息、主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。
- 而推动式消费不需要主动调用 Consumer 的拉消息方法,在底层已经封装了拉取的调用逻辑,在用户层面看来是 Broker 把消息推送过来的,底层还是 consumer 去 broker 主动拉取消息。
集群消费(Clustering)与 广播消费(Broadcasting)
- 集群消费模式下,相同 Consumer Group 的每个 Consumer 实例平均分摊消息。
- 广播消费模式下,相同 Consumer Group 的每个 Consumer 实例都接收全量的消息。
回溯消费
回溯消费是指 Consumer 已经消费成功的消息,由于业务上需求需要重新消费,要支持此功能,Broker 在向 Consumer 投递成功消息后,消息仍然需要保留。并且重新消费一般是按照时间维度,例如由于 Consumer 系统故障,恢复后需要重新消费 1 小时前的数据,那么 Broker 要提供一种机制,可以按照时间维度来回退消费进度。
消息(Message)与 标签(Tag)
最基本的信息数据,MQ 所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。每个消息拥有唯一的 Message ID,且携带具有业务标识的 Key。系统提供了通过 Message ID 和 Key 查询消息的功能。消息具备不可变性,在初始化发送和完成存储后即不可变。
而 Tag 简单说就是用来过滤筛选指定的消息的。用于同一主题下区分不同类型的消息。可以根据不同业务目的在同一主题下设置不同标签,然后消费者根据 Tag 实现对不同主题的不同消费逻辑。
消息顺序指的是一类消息消费时,能按照发送的顺序来消费,分为全局顺序消息与分区顺序消息,全局顺序是指某个Topic下的所有消息都要保证顺序,部分顺序消息只要保证每一组消息被顺序消费即可。
- 普通顺序消费(Normal Ordered Message),消费者通过同一个消息队列收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的。比如:创建订单同时让发送其他消息去发短信。
- 严格顺序消息(Strictly Ordered Message),消费者收到的所有消息均是有顺序的。例如:一个订单产生了三条消息分别是订单创建、订单付款、订单完成。消费时要按照这个顺序消费才能有意义,同时订单之间是可以并行消费的
消息事务 与 定时消息
消息事务就是分布式事务,指应用本地事务和发送消息操作可以被定义到全局事务中,要么同时成功,要么同时失败。
定时消息(延迟队列)是指消息发送到 broker 后,不会立即被消费,等待特定时间投递给真正的topic。比如内容管理系统可以在某个时间段发布文章。
消息重试、消息重投
消息重试是指 Consumer 在消费失败后,Rocket MQ 提供的一种重试机制来令消息再消费一次。
消息重投就是 Producer 在发送消息时,同步消息失败会重投,异步消息有重试,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但可能会造成消息重复,消息重复在RocketMQ中是无法避免的问题。
流量控制 与 死信队列
生产者流控,因为 broker 处理能力达到瓶颈;消费者流控,因为消费能力达到瓶颈。
死信队列(Dead-Letter Queue)用于处理无法被正常消费的消息。当一条消息初次消费失败,消息队列会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。
演示
依赖与配置
添加依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
配置文件
spring:
application:
name: mq-server
logging:
file:
name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径
# rocketmq 配置项,对应 RocketMQProperties 配置类
rocketmq:
name-server: 127.0.0.1:9876 # RocketMQ Namesrv 地址。如果多个,使用逗号分隔。
# RocketMQAutoConfiguration 自动化配置类,RocketMQ 的自动配置,创建相应的 Producer 和 Consumer
producer: # RocketMQ Producer
group: ${spring.application.name}_PRODUCER # 生产者分组
send-message-timeout: 3000 # 发送消息超时时间,单位:毫秒。默认为 3000 。
compress-message-body-threshold: 4096 # 消息压缩阀值,当消息体的大小超过该阀值后,进行消息压缩。默认为 4 * 1024B
max-message-size: 4194304 # 消息体的最大允许大小。。默认为 4 * 1024 * 1024B
retry-times-when-send-failed: 2 # 同步发送消息时,失败重试次数。默认为 2 次。
retry-times-when-send-async-failed: 2 # 异步发送消息时,失败重试次数。默认为 2 次。
retry-next-server: false # 发送消息给 Broker 时,如果发送失败,是否重试另外一台 Broker 。默认为 false
access-key: # Access Key ,可阅读 https://github.com/apache/rocketmq/blob/master/docs/cn/acl/user_guide.md 文档
secret-key: # Secret Key
enable-msg-trace: true # 是否开启消息轨迹功能。默认为 true 开启。可阅读 https://github.com/apache/rocketmq/blob/master/docs/cn/msg_trace/user_guide.md 文档
customized-trace-topic: RMQ_SYS_TRACE_TOPIC # 自定义消息轨迹的 Topic 。默认为 RMQ_SYS_TRACE_TOPIC 。
# Consumer 配置项
consumer:
listeners: # 配置某个消费分组,是否监听指定 Topic 。结构为 Map<消费者分组, <Topic, Boolean>> 。默认情况下,不配置表示监听。
test-consumer-group:
topic1: false # 关闭 test-consumer-group 对 topic1 的监听消费
项目工程
大多数情况下,生产者和消费者其实不在相同工程里面。演示就无所谓了
启动类
@SpringBootApplication
public class MQApplication {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
基本消息实体
@Data
@Accessors(chain = true) // 这里可以直接链式调用
public class TestMessage {
public static final String TOPIC = "test1";
private Integer id;
}
Producer
@Slf4j
@Component
public class Producer {
@Resource
private RocketMQTemplate rocketMQTemplate;
public SendResult syncSend(Integer id) {
TestMessage message = new TestMessage().setId(id);
// 同步发送消息
return rocketMQTemplate.syncSend(TestMessage.TOPIC, message);
}
public void asyncSend(Integer id, SendCallback callback) {
TestMessage message = new TestMessage().setId(id);
// 异步发送消息
rocketMQTemplate.asyncSend(TestMessage.TOPIC, message, callback);
}
public void onewaySend(Integer id) {
TestMessage message = new TestMessage().setId(id);
// oneway 发送消息
rocketMQTemplate.sendOneWay(TestMessage.TOPIC, message);
}
}
RocketMQTemplate 提供了多种发送方式与方法,有需要可以研究下具体实现类。这里就不多做赘述
Consumer
@Component
@Slf4j
// 这里 声明 tocpi 是 test1, 而 消费者分组是 test-consumer-group-test1
// 一般情况下建议一个消费者分组仅消费一个 Topic 。好处是每个消费者分组职责单一,只消费一个 Topic。
// 而每个消费者分组是独占一个线程池,这样能够保证多个 Topic 隔离在不同线程池,保证隔离性,
// 从而避免一个 Topic 消费很慢,影响到另外的 Topic 的消费。
@RocketMQMessageListener(consumerGroup = "test-consumer-group" + TestMessage.TOPIC,
topic = TestMessage.TOPIC)
public class Consumer implements RocketMQListener<TestMessage>{ // 泛型的好处就是不用去转
@Override
public void onMessage(TestMessage message) {
log.info("收到消息: " + message);
}
}
Consumer2
@Component
// 这里分组跟上面不一样,用于测试集群消费,同一个 Topic 下不同分组的消费
@RocketMQMessageListener(
topic = TestMessage.TOPIC,
consumerGroup = "test2-consumer-group-" + TestMessage.TOPIC
)
// RocketMQ 内置的 MessageExt 类。通过 MessageExt 可以获取到消息的更多信息,例如所属队列、创建时间等,不过消息的内容 body 就需要自己去反序列化。一般情况下不会使用 MessageExt 类。
public class Demo01AConsumer implements RocketMQListener<MessageExt> {
private Logger logger = LoggerFactory.getLogger(getClass());
@Override
public void onMessage(MessageExt message) {
logger.info("[onMessage][线程编号:{} 消息内容:{}]", Thread.currentThread().getId(), message);
}
}
注意,集群消费上面已经说了,相同 Consumer Group 的每个 Consumer 实例平均分摊消息。也就是说,发送一条 Topic 为 “test1” 的消息,可以分别被 “test-consumer-group-test1” 和 “test2-consumer-group-test1” 都消费一次。
但是,如果启动两个该演示项目的实例,则消费者分组 “test-consumer-group-test1” 和 “test2-consumer-group-test1” 里面就有多个 Consumer 了。此时,再发送一条 Topic 为 “test1” 的消息,只会被 “test-consumer-group-test1” 和 “test2-consumer-group-test1” 的一个 Consumer 消费一次,那另外一个就消费不到了。
通过集群消费的机制,可以实现针对相同 Topic ,不同消费者分组实现各自的业务逻辑。例如说:用户注册成功时,发送一条 Topic 为 “USER_REGISTER” 的消息。然后,不同模块使用不同的消费者分组,订阅该 Topic ,实现各自的拓展逻辑:
- 积分模块:判断如果是手机注册,给用户增加 20 积分。
- 优惠劵模块:因为是新用户,所以发放新用户专享优惠劵。
这样,我们就可以将注册成功后的业务拓展逻辑,实现业务上的解耦,未来也更加容易拓展。同时,也提高了注册接口的性能,避免用户需要等待业务拓展逻辑执行完成后,才响应注册成功。
主要类说明
RocketMQTemplate
RocketMQTemplate
继承 Spring Messaging 定义的 AbstractMessageSendingTemplate
抽象类,以达到融入 Spring Messaging 体系中。
在 RocketMQTemplate
中,会创建一个 DefaultMQProducer
生产者 producer ,RocketMQTemplate
后续都使用它的各种发送消息的方法。当然,因为 RocketMQTemplate
的封装,所以我们可以像使用 Spring Messaging(如 redistemplate 等) 一样的方式,进行消息的发送,而无需直接使用 RocketMQ 提供的 Producer 发送消息。
而通过 RocketMQUtil
的 convertToRocketMessage()
将消息 Message 序列化,方法如下:
// RocketMQTemplate.java
public SendResult syncSend(String destination, Object payload, long timeout) {
// RocketMQTemplate 通过 Spring Messaging 的 MessageBuilder 将我们传入的消息 payload 转换成 Spring Messaging 的 Message 消息对象。
Message<?> message = MessageBuilder.withPayload(payload).build();
}
// RocketMQUti.java
public static org.apache.rocketmq.common.message.Message convertToRocketMessage(
MessageConverter messageConverter, String charset,
String destination, org.springframework.messaging.Message<?> message) {
Object payloadObj = message.getPayload();
byte[] payloads;
try {
if (null == payloadObj) {
throw new RuntimeException("the message cannot be empty");
}
// 如果是 String 类型,则直接获得其 byte[] 内容。
if (payloadObj instanceof String) {
payloads = ((String)payloadObj).getBytes(Charset.forName(charset));
// 如果是 byte[] 类型,则直接使用即可
} else if (payloadObj instanceof byte[]) {
payloads = (byte[])message.getPayload();
// 如果是复杂对象类型,则使用 MessageConverter 进行转换成字符串,然后再获得字符串的 byte[] 内容。
} else {
String jsonObj = (String)messageConverter.fromMessage(message, payloadObj.getClass());
if (null == jsonObj) {
throw new RuntimeException(String.format(
"empty after conversion [messageConverter:%s,payloadClass:%s,payloadObj:%s]",
messageConverter.getClass(), payloadObj.getClass(), payloadObj));
}
payloads = jsonObj.getBytes(Charset.forName(charset));
}
} catch (Exception e) {
throw new RuntimeException("convert to RocketMQ message failed.", e);
}
// 转换成 RocketMQ Message
return getAndWrapMessage(destination, message.getHeaders(), payloads);
}
因为我们一般消息都是复杂对象类型,所以会采用 MessageConverter
类进行转换。默认使用 MappingJackson2MessageConverter
或 MappingFastJsonMessageConverter
,即使用 JSON 格式序列化和反序列化 Message 消息内容。为什么是这两个 MessageConverter ,可以看看 RocketMQ-Spring 的 MessageConverterConfiguration 配置类。
@RocketMQMessageListener
通过 @RocketMQMessageListener
注解,设置每个消费者 Consumer
的消息监听器的配置。
/**
* Consumer 所属消费者分组
*/
String consumerGroup();
/**
* 消费的 Topic
*/
String topic();
/**
* 选择器类型。默认基于 Message 的 Tag 选择。
* @see SelectorType
*/
SelectorType selectorType() default SelectorType.TAG;
/**
* 选择器的表达式。
* 设置为 * 时,表示全部。
*
* 如果使用 SelectorType.TAG 类型,则设置消费 Message 的具体 Tag 。
* 如果使用 SelectorType.SQL92 类型,可见 https://rocketmq.apache.org/rocketmq/filter-messages-by-sql92-in-rocketmq/ 文档
*/
String selectorExpression() default "*";
/**
* 消费模式。可选择并发消费,还是顺序消费。
*/
ConsumeMode consumeMode() default ConsumeMode.CONCURRENTLY;
/**
* 消息模型。可选择是集群消费,还是广播消费。
*/
MessageModel messageModel() default MessageModel.CLUSTERING;
/**
* 消费的线程池的最大线程数
*/
int consumeThreadMax() default 64;
/**
* 消费单条消息的超时时间 default 30s.
*/
long consumeTimeout() default 30000L;
@ExtRocketMQTemplateConfiguration
开发者可能需要连接多个不同的 RocketMQ 集群,提供了 @ExtRocketMQTemplateConfiguration
注解,实现配置连接不同集群的 Producer 的 RocketMQTemplate
Bean 对象。
// 设置连接的 RocketMQ Namesrv 地址
@ExtRocketMQTemplateConfiguration(nameServer = "${test.rocketmq.extNameServer:test.rocketmq.name-server}")
// 继承 RocketMQTemplate 类,从而可以直接使用 @Resource 注解,注入 RocketMQTemplate Bean 属性。
public class ExtRocketMQTemplate extends RocketMQTemplate {
}