中间件是什么
中间件是一种软件,支持在分布式网络中的应用或应用组件之间建立一种或多种通信或连接
避免消息丢失
拒绝自动应答 拒绝手动批量 搞手动应答(那么直接答应,要么直接拒绝 绝对不能不处理)
自动应答/手动应答区别
所谓自动应答,是指broker(代理)不在乎消费者对消息处理是否成功,都会告诉队列删除消息。如果处理消息失败,又没有捕获异常,则会实现自动补偿(队列重新向消费者投递消息);
所谓手动应答,是指消费者处理完业务逻辑之后,手动返回ack(通知)告诉broker消息处理完了,你可以删除消息了;告诉broker消息处理失败,你先别删除消息。
多种模式
每个人改做的事情:
生产者做的事情:简单说就是让队列持久,让消息持久
消费者:手动应答 拒绝批量
如何解决消息堆积
消息堆积原因
- 消息堆积即消息没及时被消费,是生产者生产消息速度快于消费者消费的速度导致的
预防措施
生产者
- 减少发布频率
- 考虑使用队列最大长度限制
- 使用集群
消费者
- 增加消费者数量:通过增加消费者的数量,可以提高消息的消费速度,从而减少消息堆积的问题。可以根据实际情况动态调整消费者的数量,以适应消息的处理需求。
- 使用惰性队列:惰性队列是一种特殊的队列,它会在消息被消费之前不会将消息发送给消费者。这样可以避免消息堆积的问题,因为只有当消费者准备好接收消息时,消息才会被发送给消费者。使用惰性队列可以有效地解决消息堆积问题。
12
点对点模式
其实就是一对一 生产者和消费者直接互通 没有中间件
流量削峰
简单来说就是限流
削峰从本质上来说就是更多地延缓用户请求,以及层层过滤用户的访问需求,遵从“最后落地到数据库的请求数要尽量少”的原则。
应用解耦
模块之间的调用十分复杂,为了降低模块与模块之间调用依赖,使用MQ将两系统分开,不直接调用系统接口,减轻两系统依赖关系
异步操作
简单来说,异步操作允许程序在等待某个任务完成的同时,继续执行其他任务,而不需要等待该任务完成后再执行下一个任务。
异步通讯
发微信可以同时操作
同步操作
打电话只能一对一 所有时效性强
优点 时效性强
缺点:高耦合 消耗资源 级联失败 性能下降
同步操作是指在执行某个任务时,必须等待该任务完成后才能继续执行下一个任务。在同步操作中,程序会按照顺序依次执行每个任务,并且每个任务的执行时间可能会影响到整个程序的执行效率。
BIO、NIO、AIO、同步异步、
BIO是阻塞式的:当一个线程运行时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
NIO是非阻塞的:当有多个窗口时,你就可以直接去到其他窗口进行操作,如果没有。那和BIO没啥区别
AIO异步非阻塞 每个连接发送过来的请求,都会绑定一个Buffer,然后通知操作系统去完成异步的读,这个时间你就可以去做其他的事情,不需要等他做完
阻塞非阻塞 线程如果满了就去其他线程 这就是非阻塞反之
如何保证数据不丢失
消息从生产端到消费端消费要经过3个步骤:
- 生产端发送消息到RabbitMQ;
- RabbitMQ发送消息到消费端;
- 消费端消费这条消息;
1. 消息在写到消息队列的过程中丢失
消息生产者一般就是业务系统,消息队列是单独部署了在独立的服务器上的,所以业务服务器和消息队列服务器可能会出现网络抖动,当出现了网络抖动,消息就会丢失
解决方案:
一般这种情况,我们可以采用消息重传的方案,即当我们发现发送的消息超时后,我们就重新发送一次,但是不能一直无限制的重传消息。按照经验来说,如果不是消息队列本身故障,或者是网络断开了,一般重试个 2 到 3 次就行了。
confirm消息确认机制 RabbitMQ(瑞比teMQ)
顾名思义,就是生产端投递的消息一旦投递到RabbitMQ后,RabbitMQ就会发送一个确认消息给生产端,让生产端知道我已经收到消息了,否则这条消息就可能已经丢失了,需要生产端重新发送消息了。
生产者:
1消息持久
2发布确认
消费者:
手动确认 禁止批量手动
ex: 选择合适的队列长度 合适的交换机 让队列持久,可以在正常里面配一套死信 也可以加个重试,在配一个消费者 去死信里面去消费
宕机
消息持久化
RabbitMQ收到消息后将这个消息暂时存在了内存中,如果RabbitMQ挂了,那重启后数据就丢失了,所以相关的数据应该持久化到硬盘中,在Spring Boot中消息默认就是持久化的。
基于什么协议
是基于amqp协议
什么是AMQP协议
是应用间的消息通信一种协议 与语言和平台无关
其他的消息队列
ActiveMQ/RabbitMQ/RocketMQ/KafKa 激活 兔子 火箭 卡夫卡
ActiveMQ(ai可t无) RabbitMQ(rua币t)RocketMQ(rua剋t)
kafka:Kafka 主要特点是基于Pull 的模式来处理消息消费,追求高吞吐量,大型公司建议可以选用,如果有日志采集功能, 肯定是首选 kafka 了。
RocketMQ 天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款
在大量交易涌入时,稳定性上可能更值得信赖,这些业务 场景在阿里双 11 已经经历了多次考验
RabbitMQ 能好时效性微秒级,社区活跃度也比较高,管理界面用起来十分 方便,如果你的数据量没有那么大就可以选它
消息队列的组成
生产者(Producer):负责产生消息。
消费者(Consumer):负责消费消息
消息代理(Message Broker):负责存储消息和转发消息两件事情。其中,转发消息分为推送和拉取两种方式。
拉取(Pull),是指 Consumer 主动从 Message Broker 获取消息
推送
什么是消息队列
是一种应用间的通信方式,消息发送后可以立即返回,有消息系统来确保信息的可靠专递,消息发布者只管把消息发布到MQ中而不管谁来取,消息使用者只管从MQ中取消息而不管谁发布的,这样发布者和使用者都不用知道对方的存在。
应用场景:
高并发
四者之间的区别
ActiveMQ 支持java语言优先 吞吐量万级
RabbitMQ 各方面都很平衡 跨平台特性 与语言无关 单位也是万级别 微秒级 低 amqp
RocketMQ 只支持java 消息延迟毫秒级 吞吐量十万级 理论上不会丢失 jws
KafKa 支持Java优先 消息延迟毫秒级 吞吐量百万级 理论上不会丢失 tbc
消息应答
消息应答就是:消费者在接收到消息并且处理该消息之后,告诉 rabbitmq 它已经处理了,rabbitmq 可以把该消息删除了。
自动应答
消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡
没有对传递的消息数量进行限制,当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终使 得内存耗尽
所以这种模式仅适用在消费者可以高效并以 某种速率能够处理这些消息的情况下使用。
手动消息应答的方法
要么直接答应,要么直接拒绝 或者不处理
消息自动重新入队
如果消费者由于某些原因失去连接,导致消息未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队
消费预取上限
因此希望开发人员能限制此缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题
什么是rabbitmq:
RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件),由以高性能、健壮以及可伸缩性出名
MQ
MQ(Message Queue)是典型的生产者消费者模型,没有业务逻辑侵入,实现生产者和消费者的解耦。它具有低耦合、可靠投递、广播、流量控制、最终一致性等一系列功能,成为异步RPC的主要手段之一。
MQ是消息通信的模型,并发具体实现。现在实现MQ的有两种主流方式:AMQP、JMS。
JMS
RabbitMQ持久化
就是mq退出或者某种情况崩溃 它忽视了队列和消息 为了确保消息不好丢失 我们就需要将队列和消息都标记为持久化
- 单独发布消息
同步等待确认,简单,但吞吐量非常有限。
- 批量发布消息
批量同步等待确认,简单,合理的吞吐量,一旦出现问题但很难推断出是那条消息出现了问题。
- 异步处理
最佳性能和资源使用,在出现错误的情况下可以很好地控制,但是实现起来稍微难些
延迟队列、重试队列、死信队列的区别
延迟队列
是指将消息延迟一段时间后再投递给消费者的队列。需要延迟处理订单超时未支付、秒杀活动结束后未支付的订单 就相当于 生产者这边发送消息之后 就可以不用再去管了,因为是异步操作,生产者发送消息给的是交换机 交换机会延迟一段时间在发给消费者队列
重试队列
是指在消息处理失败后,将消息重新投递给消费者进行重试的队列。消息处理失败网络异常、业务处理失败最大重试次数重试间隔
死信队列
是指无法被消费者成功处理的消息最终被投递到的队列。无法处理消息处理失败达到最大重试次数、消息过期记录日志、发送告警
延迟队列绑定了死信队列和重试机制的重试队列,那消息会进入到死信队列还是重试后进入重试队列呢
消息的处理流程
首先,让我们来看一下消息的处理流程。当消息发送到延迟队列时,根据设置的延迟时间进行等待。等待时间过后,如果消息未被消费者消费,则会进入绑定的死信队列。如果消费者消费了消息,但消息处理失败,消息会被发送到绑定的重试队列,进行重试操作。如果在重试队列中仍然无法处理成功,消息最终会被发送到死信队列。这种处理流程可以有效地处理消息处理失败的情况,确保消息能够被正确处理。
死信的来源
消息 过期
队列满了,无法再添加数据到 mq 中
消息被拒绝
核心原理
就是某消费者有正常和死信2套配置,然后没消费让消息去死信,然后另外一个消费者直接去死信消费
三 实现
1. RabbitMQ
是基于AMQP 协议的 具有跨语言的特性,支持多种开发语言,基于erlang语言编写,天生具有高并发.
2. rocketMQ
是基于JMS的 是阿里巴巴旗下开发的mq,只能用java语言,声称可用性极高,消息从来不会丢失.
连接 信道 交换机 队列
消息队列简单入门
首先建立两个工程 一个生产者 一个消费者
第二步 导包 两个一样的包
<dependencies>
<!--RabbitMQ 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--rabbitmq 依赖客户端-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.10.0</version>
</dependency>
<!--继承大哥-->
<dependency>
<groupId>org.example</groupId>
<artifactId>common-service</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--操作文件流的一个依赖-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<!--远程调用-->
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
<!--knife4j-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-micro-spring-boot-starter</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-data-mongodb</artifactId>-->
<!-- </dependency>-->
<!-- //服务注册与发现-->
</dependencies>
配置yaml 两个也是一样的 记得改掉端口号就行,记得上云
spring:
application:
name: producer-service
rabbitmq:
password: root
username: root
virtual-host: root
host: 192.168.116.131
port: 5672
jackson:
date-format: yyyy-MM-dd
time-zone: GMT+8
cloud:
nacos:
discovery:
server-addr: 192.168.116.131:8848
server:
port: 8088
然后建立连接 但是需要写在大哥里面 这样子可以直接继承 不然,没写一个都要建立 好麻烦 所以写在大哥里面 封装成一个工具类 然后继承大哥 直接调用
所以大哥也需要导入消息队列的包
<parent>
<!--大哥继承父亲-->
<artifactId>t167alibaba</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>common-service</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- <!–rabbitmq 依赖客户端–>-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.10.0</version>
</dependency>
<!-- <!–操作文件流的一个依赖–>-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<!--mybatisPlus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!--代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
</dependency>
<!--MybatisPlus模板-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
</dependencies>
导完之后配置工具类
public class MqUtils {
//得到一个连接的 channel
public static Channel getChannel() throws Exception {
//创建一个连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("192.168.116.131");
connectionFactory.setUsername("root");
connectionFactory.setPassword("root");
connectionFactory.setVirtualHost("root");
Connection connection = connectionFactory.newConnection();
Channel channel = connection.createChannel();
return channel;
}
}
这个时候 就可已写发送消息接收消息了
public class Hello1 {
public static void main(String[] args) throws Exception {
//1.得到信道 建立连接
Channel channel = MqUtils.getChannel();
//2.拿到今天队列的名字
String queuename="GD";
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化 也就是是否用完就删除
* 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
* 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
* 5.其他参数
*/
//3.信道声明一个队列 其实是建立和消费者的连接
channel.queueDeclare(queuename,true,false,false,null);
//4.声明你要发送的消息
String mes="罗柏亮好帅";
/**
* 发送一个消息
* 1.发送到那个交换机
* 2.路由的 key 是哪个
* 3.其他的参数信息 MessageProperties.PERSISTENT_TEXT_PLAIN持久化
* 4.发送消息的消息体 通过文件流的形式来拿到 mes.getBytes()转成二进制
*/
//发送消息
channel.basicPublish("",queuename, MessageProperties.PERSISTENT_TEXT_PLAIN,mes.getBytes());
System.out.println("消息发送成功");
}
}
接受消息
public class hello2 {
public static void main(String[] args) throws Exception {
//1.得到信道 建立连接
Channel channel = MqUtils.getChannel();
//2.拿到生产者的名字
String queuename="GD";
System.out.println("C1等待接收消息.........");
/**
* 消费者消费消息 - 接受消息
* 1.消费哪个队列
* 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
* 3.消费者未成功消费的回调
* 4.消息被取消时的回调()->{} consumerTag消费的标志 delivery接收者
*
* 轮询规则
*/
//3.接收消息
channel.basicConsume("GD",false,(consumerTag, delivery) -> {
try {
Thread.sleep(50000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String message = new String(delivery.getBody());
//取消手动批量
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false); //basicAck应答方式
System.out.println(message);//手动应答
},(consumerTag) -> {
System.out.println("消息消费被中断");
});
}
}
启动类:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class,scanBasePackages = {"com.cn.consumer",
"com.cn.common"})
@EnableFeignClients
@EnableDiscoveryClient
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class,args);
}
}
----------------------------------------------------------------------------------------------
第二次消息队列带了一点死信 延迟
广播交换机---用的少
结构目录
首先写c1 ---消费者(谁先建立的队列谁先起)广播:没有 路由键 临时队列 无名交换机
接着c2
然后生产者
第二种 直连 (用的多)它是符合条件的才可以执行,定路由键(不能路由键一样,否则就是广播)(他们的队列是消费者自己指定的)
项目结构
c1
c2
p1
第三种(主题) 主题 *.xx.* #.xx.# 模糊路由键 (不能直接写* #,否则就是广播)(他们的队列是消费者自己指定的)
项目结构
c1
c2
p
----------------------------------------------------------------------------------------------
第三次课 延迟 重试 死信
延迟:
项目结构
package com.cn.consumer.dead;
import com.cn.common.config.MqUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.util.HashMap;
import java.util.Map;
public class Consumer01 {//主要是为了把队列和交换机建好
//普通交换机名称
private static final String NORMAL_EXCHANGE = "normal_exchange";
//死信交换机名称
private static final String DEAD_EXCHANGE = "dead_exchange";
public static void main(String[] args) throws Exception {
Channel channel = MqUtils.getChannel();
//绑定交换机exchangeDeclare 声明死信和普通交换机 类型为 direct
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE,BuiltinExchangeType.DIRECT);
//声明死信队列
String deadQueue="dead-queue";
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化 也就是是否用完就删除
* 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
* 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
* 5.其他参数
*/
channel.queueDeclare(deadQueue,false,false,false,null);
//死信队列绑定:队列、交换机、路由键(routingKey) queueBind
channel.queueBind(deadQueue,DEAD_EXCHANGE,"lisi");
//正常队列绑定死信队列信息
Map<String, Object> params = new HashMap<>();
//正常队列设置死信交换机 参数 key 是固定值
params.put("x-dead-letter-exchange", DEAD_EXCHANGE);
//正常队列设置死信 routing-key 参数 key 是固定值
params.put("x-dead-letter-routing-key", "lisi");
//声明正常队列
String normalQueue = "normal-queue";
channel.queueDeclare(normalQueue, false, false, false, params); //把死信列队绑进来
channel.queueBind(normalQueue, NORMAL_EXCHANGE, "zhangsan");
System.out.println("等待接收消息........... ");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("Consumer01 接收到消息" + message);
};
/**
* 消费者消费消息 - 接受消息
* 1.消费哪个队列 deadQueue
* 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
* 3.消费者未成功消费的回调
* 4.消息被取消时的回调()->{}
*/
//这个地方必须自动应答 如果是手动 很有可能被其他人抢掉,但是如果是自动,那就指定是你一个
channel.basicConsume(normalQueue, true, deliverCallback, consumerTag -> {
});
}
}
package com.cn.consumer.dead;
import com.cn.common.config.MqUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
public class Consumer02 { //专门接受失效的消息 这个是直接去死信里面去看
//死信交换机名称
private static final String DEAD_EXCHANGE = "dead_exchange";
public static void main(String[] args) throws Exception {
Channel channel = MqUtils.getChannel();
//绑定交换机 exchangeDeclare
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
//声明队列
String deadQueue = "dead-queue";
//生成一个队列
/**
* 生成一个队列
* 1.队列名称
* 2.队列里面的消息是否持久化 也就是是否用完就删除
* 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
* 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
* 5.其他参数
*/
channel.queueDeclare(deadQueue,false,false,false,null);
//死信队列绑定:队列、交换机、路由键(routingKey) queueBind
channel.queueBind(deadQueue,DEAD_EXCHANGE,"lisi");
System.out.println("等待接收死信消息........... ");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("Consumer02 接收到消息" + message);
};
/**
* 消费者消费消息 - 接受消息
* 1.消费哪个队列 deadQueue
* 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
* 3.消费者未成功消费的回调
* 4.消息被取消时的回调()->{}
*/
channel.basicConsume(deadQueue, true, deliverCallback, consumerTag -> {
});
}
}
package com.cn.consumer.dead;
import com.cn.common.config.MqUtils;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
public class Producer {
private static final String NORMAL_EXCHANGE = "normal_exchange";//正常交换机
public static void main(String[] args) throws Exception {
Channel channel = MqUtils.getChannel();
//绑定交换机 exchangeDeclare
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT); //类型直连
//设置消息的 TTL 时间 10s
AMQP.BasicProperties build = new AMQP.BasicProperties().builder().expiration("10000").build();
//builder构建 build再次构建 得到一个带了过期属性的返回值
for (int i=1;i<11;i++){
String message = "info" + i;
/**
* 发送一个消息
* 1.发送到那个交换机
* 2.路由的 key 是哪个
* 3.其他的参数信息 MessageProperties.PERSISTENT_TEXT_PLAIN持久化
* 4.发送消息的消息体 通过文件流的形式来拿到 mes.getBytes()转成二进制
*/
channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", build, message.getBytes());
System.out.println("生产者发送消息:" + message);
}
}
}
延迟队列
进入到该文件所在的目录里面在去下载
dockers命令:
docker exec -it 1e6b78f7573b /bin/bash
docker cp rabbitmq_delayed_message_exchange-3.9.0.ez 1e6b78f7573b:/plugins //cp复制到你需要去的目录1e6b78f7573b
rabbitmq-plugins enable rabbitmq_delayed_message_exchange //插件生效
rabbitmq-plugins enable rabbitmq_shovel rabbitmq_shovel_management //生效成一个插件的管理器
生产者
因为要远程调用卡表,所以再写一个Feign类
package com.cn.producer.feign;
import com.cn.common.response.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* @Author wuchao66
* @Date 2024/1/16 15:23
* @PackageName:com.wuchao.producer.feign
* @ClassName: AccountFeign
* @Description: TODO
* @Param
* @Version 1.0
*/
@FeignClient("account-service") //指定文件
public interface AccountFeign {//模拟异步 延迟的未付
//转账
@PostMapping("transfer")
public R transfer(//必须写不然报错
@RequestParam(value = "cardNo") String cardNo,
@RequestParam(value = "otherCardNo")String otherCardNo,
@RequestParam(value = "amount")double amount);
}
写一个配置类,这个配置类是要去到mq那里面提前建好交换机队列路由键什么的 所以也写,没有交换机 你都上不去 所以后面的controller 交换机路由键的名字绑定的这边已经定义好的了
package com.cn.producer.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.CustomExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration//配置类,就和以前的配置文件差不多
//@Component//组件化
public class DelayedQueueConfig {
public static final String DELAYED_QUEUE_NAME = "delayed.queue1"; //队列的名字
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange1"; //交换机的名字
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey1"; //路由键
//就是把队列注入到ioc容器中
@Bean
public Queue delayedQueue() { //定义一个交换机 调这个方法得到一个叫做delayed.queue1名字队列
return new Queue(DELAYED_QUEUE_NAME);
}
//自定义交换机 我们在这里定义的是一个延迟交换机
@Bean
public CustomExchange delayedExchange() {//自定义延迟交换机,地板还是直连
Map<String, Object> args = new HashMap<>();
//自定义交换机的类型 x-delayed-type是什么?
args.put("x-delayed-type", "direct");//直连底板 direct(类型)
//定义交换机的名字 发送延迟消息,x-delayed-message后续参数是交换机的类型,是否持久,是否自动删除
//因为本身是没有延迟的交换机 所以把直连的底板去改成延迟交换机 挂上x-delayed-message 所以最后一个参数null填成args
return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false, args);
}
//队列绑定交换机绑定路由键 Binding=绑定 @Qualifier根据名字自定义注入
@Bean
public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue, //队列
@Qualifier("delayedExchange") CustomExchange delayedExchange) { //交换机
return BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
//BindingBuilder绑定器 绑定队列 和交换机 和路由键 noargs=没有其他参数
}
}
controller类
package com.cn.producer.controller;
import com.alibaba.fastjson.JSON;
import com.cn.common.entry.TransactionRecord;
import com.cn.common.response.R;
import com.cn.producer.feign.AccountFeign;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.Date;
@RestController
@Api("生产者")
@Slf4j
public class ProducerController {
@Resource
AccountFeign accountFeign;
@Resource
private RabbitTemplate rabbitTemplate; //兔子模板化
@ApiOperation("延迟测试消息队列转账和生成订单记录")
@PostMapping("buy")
public R buy(@RequestBody TransactionRecord transactionRecord,
@RequestParam(value = "cardNo") String cardNo,
@RequestParam(value = "otherCardNo")String otherCardNo,
@RequestParam(value = "amount")double amount,Integer delayTime){ //延迟时间delayTime
if(accountFeign.transfer(cardNo,otherCardNo,amount).getCode()==200){
//convertAndSend(…):使用此方法交换机会马上把所有的信息都交给所有的消费者,消费者再自行处理,不会因为消费者处理慢而阻塞线程。
rabbitTemplate.convertAndSend("delayed.exchange1","delayed.routingkey1", //分别是交换机的名字和路由键的名字
JSON.toJSONString(transactionRecord), correlationData -> {
correlationData.getMessageProperties().setDelay(delayTime);//ms单位setDelay设置延迟
return correlationData;
});
log.info(" 当 前 时 间 : {}, 发 送 一 条 延 迟 {} 毫秒的信息给队列 delayed.queue:{}", new Date(), delayTime,JSON.toJSONString(transactionRecord));
return R.ok();
}else {
return R.error().message("转账失败,订单无法成功");
}
}
}
消费者
要远程调用的订单 所以写一个Feign类
package com.cn.consumer.feign;
import com.cn.common.entry.TransactionRecord;
import com.cn.common.response.R;
import io.swagger.annotations.ApiOperation;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@FeignClient("record-service")
public interface RecordFeign {
@PostMapping("addTransactionRecord")
@ApiOperation("生成订单")
public R addTransactionRecord(@RequestBody TransactionRecord transactionRecord
);
}
再写一个监听器 把这个类写成一个组件
package com.cn.consumer.listener;
import com.alibaba.fastjson.JSON;
import com.cn.common.entry.TransactionRecord;
import com.cn.consumer.feign.RecordFeign;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
//写了个监听器
@Component//组件
public class Listener {
@Resource
RecordFeign recordFeign;
@RabbitListener(queues = "delayed.queue1") //写了个监听器 监听 DelayedQueueConfig 的 队列
public void get1(@Payload String message){//json @Payload支持对象变成json message变成json格式 然后在变回 对象格式=TransactionRecord.class输出
TransactionRecord transactionRecord = JSON.parseObject(message, TransactionRecord.class);
System.out.println("111"+transactionRecord.toString());//输出一下我刚刚收到了什么
//s2学过的 简单的来说就是转时间格式 LocalDateTime.now()当前时间定义
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
System.out.println("收到了消息,记录生产生产");
//把这个生成的订单记录加进去
recordFeign.addTransactionRecord(transactionRecord);
}
}