目录
一、消息中间件的产生的背景
在网络通讯中,Http请求默认采用同步请求方式,基于请求与响应模式,在客户端与服务器进行通讯时,客户端调用服务端接口后,必须等待服务端完成处理后返回结果给客户端才能继续执行,这种情况属于同步调用方式。如果服务器端发生网络延迟、不可达的情况,可能客户端也会受到影响。
二、什么是消息中间件
消息中间件利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型,它可以在分布式环境下扩展进程间的通信。对于消息中间件,常见的角色大致也就有Producer(生产者)、Consumer(消者)例如:寄快递。例如:寄快递。
三、消息中间件使用场景
1、异步处理
场景说明:用户注册后,需要发注册邮件和注册短信将注册信息写入数据库成功后,发送注册邮件,再发送注册短信,以上三个任务全部完成后,返回给客户端。
引入消息队列,改造后的架构如下:
按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间,也就是50毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此架构改变后,系统的吞吐量比串行提高了3倍,比并行提高了2倍。
2、应用解耦
场景说明:用户下单后,订单系统需要通知库存系统,传统的做法是订单系统调用库存系统的接口。
解耦合后:
订单系统:假如在下单时库存系统不能正常使用,也不影响正常下单,因为下单后,订单系统写入消息 队列就不再关心其他的后续操作了,实现订单系统与库存系统的应用解耦。
三、常见消息中间件比较
四、RocketMQ
RocketMQ是阿里巴巴开源的分布式消息中间件,现在是Apache的一个顶级项目。在阿里内部使用非常广泛,已经经过了"双11"这种万亿级的应用场景考验。
1、环境准备
(1)下载RocketMQ
官方网站:http://rocketmq.apache.org/release_notes/release-notes-4.4.0/
(2)环境要求
64位操作系统、JDK 1.8+、安装Maven。
2、安装RocketMQ
解压缩安装包、配置环境变量(变量名:ROCKETMQ_HOME,变量值:MQ解压缩路径、编辑:path,%ROCKETMQ_HOME%\bin)
3、启动RocketMQ
(1)切换到安装目录
rocketmq的bin目录下
(2)启动NameServer
start mqnamesrv.cmd
(3)启动Broker
- start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true。
- 如果弹出框提示‘错误:找不到或无法加载主类 xxxxxx。在bin下找到并打开 runbroker.cmd,然后将'%CLASSPATH%'加上英文双引号。
五、RocketMQ的架构及概念
如上图所示,整体可以分成4个角色,分别是:NameServer,Broker,Producer,Consumer。
- Broker(邮局,邮递员):Broker是RocketMQ的核心,负责消息的接收,存储,投递等功能。
- NameServer(各个邮局的管理机构):消息队列的协调者,Broker向它注册路由信息,同时Producer和Consumer向其获取路由信息。
- Producer(寄件人):消息的生产者,需要从NameServer获取Broker信息,然后与Broker建立连接,向Broker发送消息。
- Consumer(收件人):消息的消费者,需要从NameServer获取Broker信息,然后与Broker建立连接,从Broker获取消息。
- Topic(地区):用来区分不同类型的消息,发送和接收消息前都需要先创建Topic,针对Topic来发送和接收消息。
- Message Queue:为了提高性能和吞吐量,引入了Message Queue,一个Topic可以设置一个或多个Message Queue,这样消息就可以并行往各个Message Queue发送消息,消费者也可以并行的从多个 Message Queue读取消息。
- Message:Message 是消息的载体。
六、消息发送接受
1、配置坐标信息
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.4.0</version>
</dependency>
2、发送同步消息
//发送同步消息
public class RocketMQSendTest1 {
public static void main(String[] args) throws Exception {
//1. 创建消息生产者, 指定生产者所属的组名
DefaultMQProducer producer = new DefaultMQProducer("myproducer-group");
//2. 指定Nameserver地址
producer.setNamesrvAddr("127.0.0.1:9876");
//3. 启动生产者
producer.start();
for (int i = 0;i<10;i++) {
//4. 创建消息对象,指定主题、标签和消息体
Message msg = new Message("myTopic", "myTag", ("十行代码九个错误八个警告竟敢说七日精通六天学会五湖四海也不见如此三心二意之程序简直一等下流"+i).getBytes());
//5. 发送消息
SendResult sendResult = producer.send(msg);
System.out.println(sendResult);
}
//6. 关闭生产者
producer.shutdown();
}
}
首先是创建了一个DefaultMQProducer对象,DefaultMQProducer是 RocketMQ 提供的消息生产者类。"myproducer-group" 是生产者所属的组名,用于标识这个生产者的身份。组名在 RocketMQ 中用于管理和跟踪消息发送的生产者实例。在消费者中起到负载的作用。
指定了Nameserver的地址。然后启动生产者以便它可以开始发送消息,在启动之后,生产者才能与 RocketMQ Broker 进行交互。
然后进行循环发送消息到指定的Topic。这是一个同步发送方法,调用send方法后,程序会阻塞,直到 RocketMQ 返回发送结果,意味着程序会等待 RocketMQ 返回发送结果后才继续执行。在 RocketMQ 中,发送结果是由 Broker 返回的,而不是消费者。当生产者(Producer)调用 send 方法发送消息时,消息会通过网络传输到 RocketMQ 的 Broker。进行完处理消息后,会将消息的处理结果返回给生产者。这个返回的结果就是 SendResult 对象,它包含了关于消息发送的详细信息,例如消息的唯一 ID、存储位置、发送状态等。
最后关闭生产者。
总体步骤为:创建消息生产者,指定生产者所属的组名。指定Nameserver地址。启动生产者。创建消息对象,指定主题、标签和消息体。发送消息。关闭生产者。
3、接收消息
//接收消息
public class RocketMQReceiveTest1 {
public static void main(String[] args) throws Exception {
//1. 创建消息消费者, 指定消费者所属的组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("myconsumer-group");
//2. 指定Nameserver地址
consumer.setNamesrvAddr("127.0.0.1:9876");
//3. 指定消费者订阅的主题和标签
consumer.subscribe("myTopic", "*");
//CLUSTERING-clustering 集群模式 同一个消费组平均分配消息
//BROADCASTING-broadcasting 广播模式 都会接收到所有消息
// setMessageModel 用于指定消息的消费模式
consumer.setMessageModel(MessageModel.BROADCASTING);
//4. 设置回调函数,编写处理消息的方法
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
// list 参数包含了接收到的消息列表,每条消息封装在 MessageExt 对象中。
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
// 将其转换为字符串并打印出来。
System.out.println(new String(list.get(0).getBody()));
// 返回消费状态
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//5. 启动消息消费者
consumer.start();
System.out.println("Consumer Starting.");
}
}
这里首先是创建了一个消息消费者,并制定了指定了消费者所属的组名为 myconsumer-group。消费者组名用于协调消费者的行为,例如集群模式下消息的负载均衡。
指定 Nameserver 地址,然后在消费者订阅了主题 myTopic,并且订阅了该主题下所有的标签(使用 * 表示)。主题(Topic)是消息的分类,消费者可以根据主题选择订阅感兴趣的消息。标签(Tag)是主题下的细分类,* 表示订阅该主题下的所有标签的消息。
setMessageModel 用于指定消息的消费模式,CLUSTERING-clustering 集群模式 同一个消费组平均分配消息。BROADCASTING-broadcasting 广播模式同一组内都会接收到所有消息。不同组都能同时收到消息,只不过同一组受不同模式的影响会有不同的分配消息方式。
然后是注册一个消息监听器,用于处理接收到的消息。MessageListenerConcurrently是一个并发消息监听器,适用于同时处理多个消息。consumeMessage方法会被回调,当消息到达时,该方法会被调用。然后其中list参数包含了接收到的消息列表,每条消息封装在 MessageExt 对象,中。接收到的消息列表list中的第一个消息被转换为字符串并打印出来。
最后返回消息状态,CONSUME_SUCCESS表示消息处理成功,RocketMQ 会将消息标记为已消费。最后启动消费者,启动后,消费者将会根据前面配置的订阅信息和回调函数持续接收和处理消息。
测试发送同步消息(广播模式):
这里我们又创建了一个消费者,两个均为广播模式,启动生产者同步发送消息:
消费者1:
消费者2:
两个均是接收到全部的信息。
集群模式:
消费者1:
消费者2:
两个消费者平均分配消息,可能存在误差。
4、发送异步消息
异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker的响应。只会等待MQ发送状态。
//发送同步消息
//异步发送比较浪费性能,经常会失败,所以发送多几次并且让线程休眠几秒
public class RocketMQSendTest2 {
public static void main(String[] args) throws Exception {
//1. 创建消息生产者, 指定生产者所属的组名
DefaultMQProducer producer = new DefaultMQProducer("myproducer-group");
//2. 指定Nameserver地址
producer.setNamesrvAddr("127.0.0.1:9876");
//3. 启动生产者
producer.start();
for (int i = 0;i<10;i++){
//4. 创建消息对象,指定主题、标签和消息体
Message msg = new Message("myTopic", "myTag2", ("十行代码九个错误八个警告竟敢说七日精通六天学会五湖四海也不见如此三心二意之程序简直一等下流").getBytes());
//5. 发送消息
// 使用 send 方法进行异步消息发送。异步发送不会阻塞生产者线程,允许生产者继续发送其他消息。
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("发送成功:"+sendResult);
}
@Override
public void onException(Throwable e) {
System.out.println("发送异常:"+e);
}
}
);
// 这是为了给 RocketMQ 足够的时间处理消息,并避免过多的异步请求导致性能问题。
TimeUnit.SECONDS.sleep(3);
}
//6. 关闭生产者
producer.shutdown();
}
}
还是先创建消息生产者并并指定了生产者所属的组名为myproducer-group。指定地址,启动生产者。
这里用了一个匿名内部类new SendCallback()来以异步方式发送消息,onSuccess 方法在消息发送成功时被调用,打印发送结果。onException 方法在消息发送失败时被调用,打印异常信息。使用 send 方法进行异步消息发送。异步发送不会阻塞生产者线程,允许生产者继续发送其他消息。最后是为了减少异步请求的负载,代码在每次发送消息后,使线程休眠3秒。这样可以给 RocketMQ 足够的时间处理消息,并减少性能问题。
最后关闭生产者。
4、单项发送消息
//单向发送消息
public class RocketMQSendTest3 {
public static void main(String[] args) throws Exception {
//1. 创建消息生产者, 指定生产者所属的组名
DefaultMQProducer producer = new DefaultMQProducer("myproducer-group");
//2. 指定Nameserver地址
producer.setNamesrvAddr("127.0.0.1:9876");
//3. 启动生产者
producer.start();
for (int i = 0;i<10;i++){
//4. 创建消息对象,指定主题、标签和消息体
Message msg = new Message("myTopic", "myTag3", ("十行代码九个错误八个警告竟敢说七日精通六天学会五湖四海也不见如此三心二意之程序简直一等下流").getBytes());
//5. 发送消息
// 发送单向消息,没有任何返回结果
// 使用 sendOneway 方法发送消息。单向发送不会等待任何返回结果,也不会触发任何回调函数。
producer.sendOneway(msg);
TimeUnit.SECONDS.sleep(3);
}
//6. 关闭生产者
producer.shutdown();
}
}
前面的步骤均相同,他这里使用的发送消息的方法为sendOneway(msg),发送单向消息。单向发送意味着消息只会被发送到消息队列中,生产者不会等待任何响应,也不会有任何回调或返回结果。
5、消费消息
(1)负载均衡模式(默认方式)
消费者采用负载均衡方式消费消息,多个消费者共同消费队列消息,每个消费者处理的消息不同。
(2)广播模式
消费者采用广播的方式消费消息,每个消费者消费的消息都是相同的,前面已经进行了演示。
七、使用场景
接下来我们模拟一种场景:下单成功之后,向下单用户发送短信。
1、订单微服务发送消息(订单创建)
在 shop-order 服务中添加rocketmq的依赖:
<!--rocketmq-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
添加配置:
rocketmq:
name-server: localhost:9876
producer:
group: shop-order
这里producer表示它是生产者,指定了生产者所属的组名,这里配置的组名为shop-order。
编写order层控制器:
@RestController
public class OrderController {
@Autowired(required = false)
private RocketMQTemplate rocketMQTemplate;
@RequestMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
//下单(创建订单)
Order order = new Order();
order.setUid(1);
order.setUsername("测试用户");
order.setPid(pid);
order.setPname("大豫竹");
order.setPprice(2.0);
order.setNumber(1);
//下订单
System.out.println(order);
//下单成功之后,将消息放到mq中
rocketMQTemplate.convertAndSend("order-topic", order);
return order;
}
}
首先创建了一个RocketMQTemplate实例对象,它是一个 Spring 提供的一个模板类,用于简化与 RocketMQ 进行消息发送和接收的操作。,这里进行下订单操作,然后通过
2、用户订阅接收消息
然后再去user服务中去订阅消息:
在模块中添加坐标信息:
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
修改配置文件:
rocketmq:
name-server: localhost:9876
这里指定RocketMQ 的 Nameserver 地址,跟生产者对应地址一致。
然后在业务层中编写接受消息代码:
//发送短信的服务
@Component
@RocketMQMessageListener(consumerGroup = "shop-user", topic = "order-topic")
public class SmsService implements RocketMQListener<Order> {
@Override
public void onMessage(Order order){
System.out.println(order);
}
}
其中@RocketMQMessageListener是配置 RocketMQ 消息监听器的关键注解,用于指定消费组和订阅的主题。consumerGroup = "shop-user":指定了消费者所属的消费组名 "shop-user"。topic = "order-topic":指定了该监听器订阅的消息主题为 "order-topic"。监听器会接收并处理这个主题下的消息,会自动的执行下面的方法。
运行结果:
在user服务控制台中返回下单信息: