SpringCloud MQ

概述

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 部署

因为有些朋友总喜欢捣鼓怎么安装软件,这里真的不建议(纯属浪费时间),对技术无任何提升,我们是使用工具来替我们做事,不是去研究他怎么安装更正确。当然一点配置还是需要懂,但是官网有对应的参数可以参考

RocketMQ 官网

概念

参考 基本概念基本特性

简单说,就是 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 发送消息。

而通过 RocketMQUtilconvertToRocketMessage() 将消息 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 类进行转换。默认使用 MappingJackson2MessageConverterMappingFastJsonMessageConverter ,即使用 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 {
}
### 如何在 Spring Cloud 中配置和使用 MQ 进行消息发送 #### 配置依赖项 为了使项目能够支持消息队列的功能,在项目的 `pom.xml` 文件中需引入相应的 Starter 依赖。对于 RabbitMQ 或 Kafka 等不同类型的 MQ,具体的依赖会有所不同。 以 RabbitMQ 为例: ```xml <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</artifactId> </dependency> ``` 而对于 Apache Kafka,则应替换为如下依赖: ```xml <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-kafka</artifactId> </dependency> ``` 这些依赖允许开发者通过统一的方式操作不同的消息中间件[^1]。 #### 应用程序属性设置 接着需要定义应用程序的配置文件来指定连接到的消息代理服务器的信息以及绑定器和其他必要的参数。以下是针对 RabbitMQ 的一个简单例子: ```properties spring.application.name=stream-producer-app server.port=8090 # 定义输入/输出通道名称 spring.cloud.stream.bindings.output.destination=my-topic-exchange spring.cloud.stream.rabbit.bindings.output.producer.routingKeyExpression="'my-routing-key'" # 设置RabbitMQ连接信息 spring.rabbitmq.host=localhost spring.rabbitmq.port=5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest ``` 上述配置指定了目标交换机的名字、路由键表达式以及其他关于 RabbitMQ 的基本信息。 #### 编写消息发送逻辑 最后一步是在代码里编写实际的消息发布方法。这可以通过注入 `Source` 接口并调用其 `send()` 方法完成。下面给出了一段 Java 实现的例子: ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.support.MessageBuilder; import org.springframework.stereotype.Service; @Service public class MyMessageSender { @Autowired private Source source; public void sendMessage(String messageContent){ this.source.output().send(MessageBuilder.withPayload(messageContent).build()); } } ``` 这段代码展示了如何创建一条新消息并通过已配置好的输出通道将其发出。这里利用了 Spring Integration 提供的基础组件简化了消息处理流程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值