目录
消息中间件的产生的背景
在网络通讯中,Http请求默认采用同步请求方式,基于请求与响应模式,在客户端与服务器进行通讯时,客户端调用服务端接口后,必须等待服务端完成处理后返回结果给客户端才能继续执行,这种情况属于同步调用方式。如果服务器端发生网络延迟、不可达的情况,可能客户端也会受到影响
一、什么是消息中间件
消息中间件利用高效可靠的消息传递机制进行平台无关的数据交流,并基于数据通信来进行 分布式系统 的集成。通过提供消息传递和消息排队模型,它可以在分布式环境下扩展进程间的通信。对于消息中间件, 常见的角色大致也就有Producer(生产者)、Consumer(消费者)例如:寄快递
二、消息中间件使用场景
1. 异步处理
场景说明:用户注册后,需要发注册邮件和注册短信传统的做法有两种a.串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信,以上三个任务全部完成后,返回给客户端b.并行方式:将注册信息写入数据库成功后,发送注册邮件的同时发送注册短信,以上三个任务完成后,返回给客户端;与串行的差别是并行的方式可以提高处理的时间引入消息队列,改造后的架构如下按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间,也就是50 毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此架构改变后,系统的吞吐量比串行提高了3 倍,比并行提高了 2 倍
2. 应用解耦
场景说明:用户下单后,订单系统需要通知库存系统,传统的做法是订单系统调用库存系统的接口
解耦合后:订单系统:假如在下单时库存系统不能正常使用,也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了,实现订单系统与库存系统的应用解耦
三、常见消息中间件比较
特性MQ ActiveMQ RabbitMQ RocketMQ Kafka 生产者消费者模式 支持 支持 支持 支持 发布订阅模式 支持 支持 支持 支持 请求回应模式 支持 支持 不支持 不支持 Api完备性 高 高 高 高 多语言支持 支持 支持 java 支持 单吞吐量 万级 万级 万级 十万级 消息延迟 无 微秒级 毫秒级 毫秒级 可用性 高(主从) 高(主从) 非常高(分布式) 非常高(分布式) 消息丢失 低 低 理论上不会丢失 理论上不会丢失 文档的完备性 高 高 教高 高 提供快速入门 有 有 有 有 社区活跃度 高 高 中 高 商业支持 无 无 商业云 商业云
四、RocketMQ
RocketMQ 是阿里巴巴开源的分布式消息中间件,现在是 Apache 的一个顶级项目。在阿里内部使用非常广泛,已经经过了" 双 11" 这种万亿级的应用场景考验。
1. 环境准备
下载 RocketMQhttp://rocketmq.apache.org/release_notes/release-notes-4.4.0/环境要求64 位操作系统JDK 1.8+安装 Maven
2. 安装RocketMQ
解压缩安装包配置环境变量变量名:ROCKETMQ_HOME 变量值:MQ解压缩路径编辑: path %ROCKETMQ_HOME%\bin
3. 启动RocketMQ
切换到安装目录rocketmq的bin目录下启动NameServerstart mqnamesrv.cmd
启动 Brokerstart mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true
如果弹出框提示 ‘ 错误 : 找不到或无法加载主类 xxxxxx’ 。在 bin 下找到并打开runbroker.cmd ,然后将 ‘%CLASSPATH%’ 加上英文双引号
4. 安装可视化插件
- github下载
- rocketmq-externals-rocketmq-console-1.0.0.zip
- 解压压缩包
- 进入\rocketmq-console\src\main\resources文件加,用编辑器打开application.properties文件
- 进入rocketmq-externals\rocketmq-console 文件夹,执行:mvn clean package - Dmaven.test.skip=true,编译生成jar包
- 编译成功后,在rocketmq-externals\rocketmq-console下会生成target文件夹 进入target后执行:java -jar rocketmq-console-ng-1.0.0.jar,这里是在启动jar工程。
- 启动完毕后,在浏览器输入:http://localhost:8085/ 进入控制台
五、RocketMQ的架构及概念
如上图所示,整体可以分成 4 个角色,分别是: NameServer , Broker , Producer ,RocketMQ组件介绍
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是消息的载体。
六、消息发送接受
导入坐标
<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.4.0</version> </dependency
1. 发送同步消息
这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。消息发送步骤:
- 1. 创建消息生产者, 指定生产者所属的组名
- 2. 指定Nameserver地址
- 3. 启动生产者
- 4. 创建消息对象,指定主题、标签和消息体
- 5. 发送消息
- 6. 关闭生产者
/** * @author zkt * @Version 1.0 * @since 2024/7/30 */ //发送同步消息 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(); } }
2. 接收消息
消息接收步骤:
- 1. 创建消息消费者, 指定消费者所属的组名
- 2. 指定Nameserver地址
- 3. 指定消费者订阅的主题和标签
- 4. 设置回调函数,编写处理消息的方法
- 5. 启动消息消费者
/** * @author zkt * @Version 1.0 * @since 2024/7/30 */ //接收消息 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 // consumer.setMessageModel(MessageModel.BROADCASTING); //4. 设置回调函数,编写处理消息的方法 consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) { // System.out.println("获取到的消费数据: " + list); System.out.println(new String(list.get(0).getBody())); //返回消费状态 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); //5. 启动消息消费者 consumer.start(); System.out.println("Consumer Starting."); } }
3. 发送异步消息
异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待 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. 发送消息 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); } } ); TimeUnit.SECONDS.sleep(3); } //6. 关闭生产者 producer.shutdown(); } }
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. 发送消息 // 发送单向消息,没有任何返回结果 producer.sendOneway(msg); TimeUnit.SECONDS.sleep(3); } //6. 关闭生产者 producer.shutdown(); } }
这里启动两个消费者接收 并且使用 负载均衡方式 轮询cluster
5. 消费消息
1. 负载均衡模式(默认方式)消费者采用负载均衡方式消费消息,多个消费者共同消费队列消息,每个消费者处理的消息不同2. 广播模式消费者采用广播的方式消费消息,每个消费者消费的消息都是相同的//广播模式消费 consumer.setMessageModel(MessageModel.BROADCASTING);
发送方式 发送TPS 发送结果反馈 可靠性 同步发送 快 有 不丢失 异步发送 快 有 可能丢失 单向发送 最快 无 可能丢失
七、使用场景
接下来我们模拟一种场景: 下单成功之后,向下单用户发送短信
1. 订单微服务发送消息
在之前feign的代码上进行改写
1 在 shop-order服务 中添加rocketmq的依赖<!-- mq --> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> </dependency>
父项目也要导
2 添加配置
rocketmq: name-server: localhost:9876 producer: group: shop-order
3 编写测试代码
修改原有的下单服务
@RestController public class OrderController { @Autowired private IProductService productService; @Autowired private IOrderService orderService; @Autowired(required = false) private RocketMQTemplate rocketMQTemplate; //Ribbon下单 @RequestMapping("/order/prod/{pid}") public Order order(@PathVariable("pid") Integer pid) { //通过fegin调用商品微服务 Product product = productService.findByPid(pid); //下单(创建订单) Order order = new Order(); order.setUid(1); order.setUsername("测试用户"); order.setPid(pid); order.setPname(product.getPname()); order.setPprice(product.getPprice()); order.setNumber(1); orderService.createOrder(order); //下单成功之后,将消息放到mq中 rocketMQTemplate.convertAndSend("order-topic", order); return order; }
2 用户微服务订阅消息
1 修改 shop-user 模块配置
<!-- mq --> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> </dependency>
2 修改置文件rocketmq: name-server: localhost:9876
3 编写消息接收服务/** * @author zkt * @Version 1.0 * @since 2024/7/30 */ //发送短信的服务 @Service @RocketMQMessageListener(consumerGroup = "shop-user", topic = "order-topic") public class SmsService implements RocketMQListener<Order> { @Override public void onMessage(Order order) { System.out.println("这是一条短信:"+order); } }
4 启动服务,执行下单操作,观看后台输出
下单成功
发送短信成功