SpringBoot 场景开发多面手成长手册 小册笔记
整合RocketMQ
在开始运行 RocketMQ 之前,我们先思考一个实际的场景。
假设我们项目中有一个消息的生产者和消费者,它们连接到一个 RocketMQ 实例上,如下图所示。
随着业务规模的不断扩大,一个 RocketMQ 的实例已经有些不堪重负,于是我们需要将单机版的 RocketMQ 改为 RocketMQ 集群,此时我们不仅需要对 RocketMQ 扩容,还需要改变生产者和消费者的配置,让它们都连接到所有的 RocketMQ 实例上,如下图所示。
这样简单的扩容后会产生一个问题:如果 RocketMQ 的实例不断变动,那么消息的生产者和消费者会不断的修改配置,疲于应对 RocketMQ 集群的变动。
如何改善这种麻烦的现状呢?参照 SpringCloud 中服务注册与治理中心的思维,如果可以引入一个 RocketMQ 的注册中心,之后生产者和消费者都直接去连接注册中心,那是不是可以解决呢?
当然可以,RocketMQ 中就有一个对应的组件:NameServer 命名服务器,由这个 NameServer 来收集所有注册上线的 RocketMQ 实例(broker)。生产者和消费者只需要连接到 RocketMQ 的 NameServer 即可获得当前存活的 broker ,无需再因为 broker 扩容或者变动而改动配置。
如此了解下来,我们就能知道,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 的反馈,所以消息是否真的发成功了也不知道。(简单来讲一句话:别管好不好使,你就说快不快吧)
消息发送和接收机制
发送机制
- 当消息的生产者要把消息发送到 RocketMQ 中,本质是发送到 Broker 中,由于我们的生产者连接的都是 NameServer 而非 Broker ,所以第一步其实是消息的生产者从 NameServer 中获取可以发送的 Broker 。
- 注意这个环节中,消息的生产者完全可以从 NameServer 中获取到 Broker 的信息,因为 Broker 在启动的时候会把自身包含的所有队列都上报给 NameServer 。生产者从 NameServer 中拿到的 broker 信息主要有三方面内容:broker 的地址、broker 的名称、内部队列的 id 。
- 每个 Broker 中又包含不同的队列容器,消息的生产者拿到所有的 Broker 后,从中选择一个合适的队列,将消息放入这个队列中,从而完成消息的发送。
- 如果消息发送失败,则下一次再发送消息时,生产者会主动避开这些失败的 broker 。
- 在消息放入队列之前,生产者会先对消息进行校验(如检查消息正文不能为空,消息不能太长等等),检查完毕后会检查选择的 broker 中是否包含消息对应的 topic ,如果没有这个 topic 则会自动创建 topic ,并且默认创建 4 个写队列和 4 个读队列。
- 一次性创建 4 个队列的目的是考虑到高可用和高性能。
消费机制
- 消息发送到 RocketMQ 后,下一步就该由消费者来消费这条消息了。消费者在启动时,也会连接到 NameServer ,匹配自己需要监听的 Broker 中的队列。
- 消费者与 Broker 建立监听关系有如下规则:
- 一个消费者组中包含多个消费者(即一个消费者组是一个集群)
- 一个消费者组可以消费多个 topic 中的多个队列(即监听多个 topic )
- 一个 Broker 中的一个 topic 中的一个队列只能被一个消费者监听(注意层级关系)
- 消费者在消费消息时有两种模式:
- 集群模式(push模式):topic 下的消息只能被一个消费者消费
- 广播模式(pull 模式):topic 下的消息会被监听的所有消费者消费
最后我们用一张图来概述上面的描述,小伙伴们可以参照这张图来辅助理解上面的描述。
消息的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 的消息没有打印。
用这种方式我们就可以实现同一套 topic 的不同 tag 的分发接收。实际在项目开发中,这种设计配合模板方法模式,可以很优雅的实现共有逻辑的抽取和个性逻辑的扩展。
自定义消息格式
上一章中我们发送的消息一直都是 String
类型的,其实我们完全可以发送一些 Entity / VO / DTO 类型的模型对象,作为数据传递的消息正文内容,对象被转换为 json 字符串保存到消息正文。
延迟消息发送
延迟消息?可能各位会觉得很奇怪,消息还有延迟一说吗?是的,在一些特殊的业务场景中,通常会设计一种延迟规则,比方说我们在点外卖的时候,如果各位点了外卖但没有支付,通常平台会告诉你需要在 15 分钟内支付,否则订单将会被取消。这种业务场景就可以使用延迟消息来实现,由于延迟消息是在指定时间之后定时触发,所以也可以称为 “定时消息” 。