SpringBoot 场景开发多面手成长手册 小册笔记

整合RocketMQ

在开始运行 RocketMQ 之前,我们先思考一个实际的场景。

假设我们项目中有一个消息的生产者和消费者,它们连接到一个 RocketMQ 实例上,如下图所示。

30. RocketMQ结构1.png

随着业务规模的不断扩大,一个 RocketMQ 的实例已经有些不堪重负,于是我们需要将单机版的 RocketMQ 改为 RocketMQ 集群,此时我们不仅需要对 RocketMQ 扩容,还需要改变生产者和消费者的配置,让它们都连接到所有的 RocketMQ 实例上,如下图所示。

30. RocketMQ结构2.png

这样简单的扩容后会产生一个问题:如果 RocketMQ 的实例不断变动,那么消息的生产者和消费者会不断的修改配置,疲于应对 RocketMQ 集群的变动。

如何改善这种麻烦的现状呢?参照 SpringCloud 中服务注册与治理中心的思维,如果可以引入一个 RocketMQ 的注册中心,之后生产者和消费者都直接去连接注册中心,那是不是可以解决呢?

当然可以,RocketMQ 中就有一个对应的组件:NameServer 命名服务器,由这个 NameServer 来收集所有注册上线的 RocketMQ 实例(broker)。生产者和消费者只需要连接到 RocketMQ 的 NameServer 即可获得当前存活的 broker ,无需再因为 broker 扩容或者变动而改动配置。

30. RocketMQ结构3.png

如此了解下来,我们就能知道,RocketMQ 中包含一个 NameServer 和若干的 Broker ,由 NameServer 负责收集 Broker 的地址信息,Broker 负责实际的消息收发和存储工作

既然是 NameServer 负责收集 Broker 的信息,那么先启动 NameServer 后启动 Broker 会更合理。

发送消息的方式

对于消息的生产者而言,RocketMQ 本身支持 3 种消息发送的方式,分别是同步发送、异步发送、单向发送。

同步发送

发送同步消息,指的是当消息的发送方将消息发送给 RocketMQ 的整个过程中,发送方的线程会一直阻塞,直到 RocketMQ 响应发送结果为止。

异步发送

相对比于同步发送,异步发送消息时,生产者把消息发给 RocketMQ 后不会阻塞等待,RocketMQ 收到消息后响应发送结果时,会在生产者中生成一个新的线程,并在这个新的线程中回调发送成功或失败。

下面我们可以编写一个异步发送消息的逻辑。RocketMQTemplate 中用来发送异步消息的方法统一是 asyncSend ,它有非常多的重载方法,我们选择一个相对简单的即可,相较于同步发送只是多了一个 SendCallback 参数(也就是发送完毕后 RocketMQ 的回调):

    public void asyncSend() {
        // 异步发送消息
        rocketMQTemplate.asyncSend("test-sender", "test async message", new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                System.out.println("异步消息发送成功");
                System.out.println(sendResult);
            }
    
            @Override
            public void onException(Throwable e) {
                System.out.println("消息发送失败,异常:" + e.getMessage());
                e.printStackTrace();
            }
        });
    }
单向发送

单向发送消息,指的是消息的生产者向 RocketMQ 发送消息后,不再阻塞等待 RocketMQ 的响应结果,发出去就算完事。这种发送方式由于不需要等待 RocketMQ 的反馈,所以它的效率是最高的;但同时由于不接收 RocketMQ 的反馈,所以消息是否真的发成功了也不知道。(简单来讲一句话:别管好不好使,你就说快不快吧)

消息发送和接收机制

发送机制
  1. 当消息的生产者要把消息发送到 RocketMQ 中,本质是发送到 Broker 中,由于我们的生产者连接的都是 NameServer 而非 Broker ,所以第一步其实是消息的生产者从 NameServer 中获取可以发送的 Broker 。
    • 注意这个环节中,消息的生产者完全可以从 NameServer 中获取到 Broker 的信息,因为 Broker 在启动的时候会把自身包含的所有队列都上报给 NameServer 。生产者从 NameServer 中拿到的 broker 信息主要有三方面内容:broker 的地址、broker 的名称、内部队列的 id
  2. 每个 Broker 中又包含不同的队列容器,消息的生产者拿到所有的 Broker 后,从中选择一个合适的队列,将消息放入这个队列中,从而完成消息的发送。
    • 如果消息发送失败,则下一次再发送消息时,生产者会主动避开这些失败的 broker 。
  3. 在消息放入队列之前,生产者会先对消息进行校验(如检查消息正文不能为空,消息不能太长等等),检查完毕后会检查选择的 broker 中是否包含消息对应的 topic ,如果没有这个 topic 则会自动创建 topic ,并且默认创建 4 个写队列和 4 个读队列。
    • 一次性创建 4 个队列的目的是考虑到高可用和高性能。
消费机制
  1. 消息发送到 RocketMQ 后,下一步就该由消费者来消费这条消息了。消费者在启动时,也会连接到 NameServer ,匹配自己需要监听的 Broker 中的队列。
  2. 消费者与 Broker 建立监听关系有如下规则:
    • 一个消费者组中包含多个消费者(即一个消费者组是一个集群)
    • 一个消费者组可以消费多个 topic 中的多个队列(即监听多个 topic )
    • 一个 Broker 中的一个 topic 中的一个队列只能被一个消费者监听(注意层级关系)
  3. 消费者在消费消息时有两种模式:
    • 集群模式(push模式):topic 下的消息只能被一个消费者消费
    • 广播模式(pull 模式):topic 下的消息会被监听的所有消费者消费

最后我们用一张图来概述上面的描述,小伙伴们可以参照这张图来辅助理解上面的描述。

30. RocketMQ的发送接收机制.png

消息的tags

tags的业务意义

为什么要有这个东西呢?我们可以思考一个现实开发的场景:

我们 RocketMQ 在一个项目中通常不可能只用于一个业务吧,肯定是好多个业务都会用到。如果只是用 topic 区分消息的话,那会产生一种问题:但凡是一个新业务场景,都会开辟一块全新的 topic ,如果这块业务又有很多的二级分类,那要么所有的消息全部一股脑接收,由消费方统一处理,要么为每一个二级分类都划分一个 topic 。

这个法可行吗?可行!优雅吗?貌似不是那么优雅。

那怎么更优雅呢?哎,这就可以用 topic + tags 的方式来解决了。比方说每个大的业务场景用不同的 topic 划分(比方说一个 ERP 系统的销售、采购类业务用不同的 topic ),而每个业务场景的分类则可以用不同的 tags 区分(如采购软件、硬件、耗材等)。

使用tags

原生 RocketMQ 使用 tags 还稍微麻烦点,但是在 SpringBoot 整合 RocketMQ 后会变得特别简单,只需要在发送 / 接收消息的 topic 后添加 ":tags" 即可。

举个例子吧,我们用一个 topic 后面挂不同的 tag ,连续发两条消息;消费者中声明只过滤 tag 为 software 的消息:

    public void sendTagsMessage() {
        // 发送tags为"software"和"hardware"的消息
        rocketMQTemplate.convertAndSend("test-tags:software", "test software message");
        rocketMQTemplate.convertAndSend("test-tags:hardware", "test hardware message");
    }
@Component
@RocketMQMessageListener(topic = "test-tags", consumerGroup = "tagsconsumer", selectorExpression = "software")
public class SoftwareMessageReceiver implements RocketMQListener<String> {
    
    @Override
    public void onMessage(String message) {
        System.out.println("收到software消息:" + message);
    }
}

以此法编写后,我们重启生产者和消费者。触发生产者的 sendTagsMessage 方法后,可以发现在 RocketMQ dashboard 中消息已经成功附加了 tag ,而且消费者的控制台中只打印了 software 的消息,hardware 的消息没有打印。

30 发送的消息已经带了tags.png

30. 只消费了software的消息.png

用这种方式我们就可以实现同一套 topic 的不同 tag 的分发接收。实际在项目开发中,这种设计配合模板方法模式,可以很优雅的实现共有逻辑的抽取和个性逻辑的扩展。

自定义消息格式

上一章中我们发送的消息一直都是 String 类型的,其实我们完全可以发送一些 Entity / VO / DTO 类型的模型对象,作为数据传递的消息正文内容,对象被转换为 json 字符串保存到消息正文。

延迟消息发送

延迟消息?可能各位会觉得很奇怪,消息还有延迟一说吗?是的,在一些特殊的业务场景中,通常会设计一种延迟规则,比方说我们在点外卖的时候,如果各位点了外卖但没有支付,通常平台会告诉你需要在 15 分钟内支付,否则订单将会被取消。这种业务场景就可以使用延迟消息来实现,由于延迟消息是在指定时间之后定时触发,所以也可以称为 “定时消息” 。

posted @ 2023-02-04 14:13  Dazzling!  阅读(85)  评论(0编辑  收藏  举报