目录
一.同步异步调用
1. 同步调用
以余额支付为例:
同步调用的优势是什么?
- 时效性强,等待到结果后才返回。
同步调用的问题是什么?
- 拓展性差
- 性能下降
- 级联失败问题
2.异步调用
异步调用方式其实就是基于消息通知的方式,一般包含三个角色:
- 消息发送者:投递消息的人,就是原来的调用方
- 消息代理:管理、暂存、转发消息,你可以把它理解成微信服务器
- 消息接收者:接收和处理消息的人,就是原来的服务提供方
异步调用的优势是什么?
- 耦合度低,拓展性强
- 异步调用,无需等待,性能好
- 故障隔离,下游服务故障不影响上游业务
- 缓存消息,流量削峰填谷
异步调用的问题是什么?
- 不能立即得到调用结果,时效性差
- 不确定下游业务执行是否成功
- 业务安全依赖于Broker的可靠性
二. MQ选型
1. MQ技术选型
MQ(MessageQueue),中文是消息队列,字面来看就是存放消息的队列。也就是异步调用中的Broker。
2.RabbitMQ的介绍和安装
(1)简介
RabbitMQ是基于Erlang语言开发的开源消息通信中间件,官网地址:https://www.rabbitmq.com/
RabbitMQ的整体架构及核心概念:
- virtual-host:虚拟主机,起到数据隔离的作用
- publisher:消息发送者
- consumer:消息的消费者
- queue:队列,存储消息
- exchange:交换机,负责路由消息
(2)安装
RabbitMQ 是一个由 Erlang 语言开发的 AMQP的开源实现。 所以安装RabbitMQ前需要安装Erlang。所以两者相互依赖,两者版本必须对应 , 这里我们安装以下版本。
左边是3.7.x是RabbitMQ版本 , 中间20.3是Erlang的最低版本 , 右边是最高版本。
我们选择安装Erlang21.3 下载链接 Erlang OTP 21.3 is released - Erlang/OTP
RabbitMQ 3.7.17 Release RabbitMQ 3.7.17 · rabbitmq/rabbitmq-server
1️⃣Erlang安装:
浏览器打开Erlang OTP 21.3 is released - Erlang/OTP(版本找起来可能比较麻烦,所以在这里我直接给出地址)
点击网址下载
下载好后,打开安装包,选择路径就一路next,记住自己的安装路径
找到安装的位置的bin文件夹,然后复制此路径
再配置环境变量: 右击此电脑 ->属性->高级系统设置->环境变量->系统变量->PATH双击->新建->粘贴复制好的bin路径
打开命令窗口 输入 erl -version:
到这一步 Erlang安装完成。
2️⃣RabbitMQ安装
浏览器直接输入https://github.com/rabbitmq/rabbitmq-server/releases/tag/v3.7.17
这是rabbitMQ 3.7.17 列表 一直下拉找到 选择exe文件 然后下载->安装->一直next。
找到安装位置 -> 找到sbin文件夹-> 在路径上输入cmd
在命令窗口输入以下命令,启动可视化插件
rabbitmq-plugins enable rabbitmq_management
再输入 rabbitmqctl status
浏览器输入:http://127.0.0.1:15672
用户名guest 密码guest
停止插件命令:
rabbitmq-plugins disable rabbitmq_management停止MQ命令:
rabbitmqctl stop开启服务:
rabbitmqctl start_app
3.快速入门
新建队列hello.queue1和hello.queue:
向默认的amp.fanout交换机发送一条消息(点击amp.fanout交换机—>绑定队列—>发消息):
查看消息是否到达hello.queue1和hello.queue2
消息发送的注意事项有哪些?
- 交换机只能路由消息,无法存储消息
- 交换机只会路由消息给与其绑定的队列,因此队列必须与交换机绑定
4.数据隔离
新建一个用户xiaoma,密码123456,administrator权限(先添加用户,再退出登录用刚添加的用户登录)
为新用户创建一个virtual host;名字为/xiaoma
测试不同virtual host之间的数据隔离现象:
发现不同virtual host之间的数据是隔离的
三. Java客户端
1. 快速入门(简单模型)
SpringAmqp的官方地址: https://spring.io/projects/spring-amqp
导入课前资料提供的Demo工程来测试
1.利用控制台创建队列simple.queue
2.引入spring-amqp依赖
引入spring-amqp依赖 在父工程中引入spring-amqp依赖,这样publisher和consumer服务都可以使用:
<!--AMQP依赖,包含RabbitMQ--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>3.配置RabbitMQ服务端信息
在每个微服务中引入MQ服务端信息,这样微服务才能连接到RabbitMQ
spring: rabbitmq: host: 127.0.0.1 #rabbitmq地址 port: 5672 #代码连接的端口,如果是网页中连接是15672 virtual-host: /xiaoma #虚拟主机 username: xiaoma #用户名 password: 123456 #密码4. 发送消息
SpringAMQP提供了RabbitTemplate工具类,方便我们发送消息。
编写 com.itheima.publisher.amqp.SpringAmqpTest 发送消息代码如下:
@SpringBootTest public class SpringAmpqTest { @Autowired private RabbitTemplate rabbitTemplate; //向队列名称为simple.queue的队列发送一个消息 @Test public void testSimpleQueue(){ //队列名称 String queueName = "simple.queue"; //消息内容 String message = "hello,spring amqp!"; //发送消息 使用简单队列的时候,可以不用指定交换机,默认使用默认交换机 //参数1: 队列名称 参数2: 要发送的消息内容 rabbitTemplate.convertAndSend(queueName,message); } }5. 接收消息
SpringAMQP提供声明式的消息监听,我们只需要通过注解在方法上声明要监听的队列名称,将来 SpringAMQP就会把消息传递给当前方法:
@Slf4j @Component public class SpringRabbitListener { //从simple.queue队列中接收消息 @RabbitListener(queues = "simple.queue") public void listenSimpleQueue(String msg) { log.info("从simple.queue队列中接收到消息:"+msg+"}"); System.out.println("spring 消费者接收到消息:{"+msg+"}"); } }发送消息后控制台就会打印:
2. Work Queues
Work queues,任务模型。简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息
1. 创建队列:
2.在publisher服务中定义测试方法,在1秒内产生50条消息,发送到work.queue
//向work.queue队列发消息,循环发送50条消息,每发一条消息沉睡20ms @Test public void testWorkQueue() throws InterruptedException { //队列名称 String queueName = "work.queue"; //消息内容 String message = "hello,spring amqp!"; //发送消息 使用简单队列的时候,可以不用指定交换机,默认使用默认交换机 //参数1: 队列名称 参数2: 要发送的消息内容 for (int i = 1; i <= 50; i++) { rabbitTemplate.convertAndSend(queueName,message+"-"+i); try { Thread.sleep(20); }catch (Exception e){ e.printStackTrace(); } } }3. 在consumer服务中定义两个消息监听者,都监听work.queue队列
//消费者1:监听work.queue队列,每次处理完一个消息沉睡20ms @RabbitListener(queues = "work.queue") public void listenWorkQueue1(String msg) throws InterruptedException { System.out.println("消费者1从work.queue队列中接收到消息:{"+msg+"}"); try { Thread.sleep(20); }catch (Exception e){ e.printStackTrace(); } } //消费者2:监听work.queue队列,每次处理完一个消息沉睡200ms @RabbitListener(queues = "work.queue") public void listenWorkQueue2(String msg) throws InterruptedException { System.out.println("---消费者2从work.queue队列中接收到消息:{"+msg+"}"); try { Thread.sleep(200); }catch (Exception e){ e.printStackTrace(); } }默认情况下,RabbitMQ的会将消息依次轮询投递给绑定在队列上的每一个消费者。但这并没有考虑到消费者是否已经处理完消息,可能出现消息堆积。 因此我们需要修改application.yml,设置preFetch值为1来控制消费者预取的消息数量,处理完一条再处理下一条,实现能者多劳,确保同一时刻最多投递给消费者1条消息:
logging: pattern: dateformat: MM-dd HH:mm:ss:SSS spring: rabbitmq: host: 127.0.0.1 port: 5672 virtual-host: /xiaoma username: xiaoma password: 123456 listener: simple: prefetch: 1 #每次从队列中获取的消息数量,处理完之后再从队列中获取查看控制台大部分都是1处理的,因为1快:
3. fanout交换机
(1)Fanout广播
FanoutExchange 会将接收到的消息广播到每一个跟其绑定的queue,所以也叫广播模式
1.新建两个队列
2.新建交换机并将两个队列与其绑定
3.编写消息监听器,监听fanout.queue1和fanout.queue2
@RabbitListener(queues = "fanout.queue1") public void listenFanoutQueue1(String msg) { System.out.println("消费者1从fanout.queue1队列中接收到消息:{"+msg+"}"); } //监听fanout.queue2队列的消息 @RabbitListener(queues = "fanout.queue2") public void listenFanoutQueue2(String msg) { System.out.println("消费者2从fanout.queue2队列中接收到消息:{"+msg+"}"); }4.编写单元测试,向hmall.fanout交换机发送消息
//发送消息到hmall.fanout的交换机 @Test public void testFanoutExchange(){ //交换机的名称 String exchangeName = "hmall.fanout"; //消息内容 String message = "hello,fanout exchange"; //参数1:交换机名称 参数2:路由key 参数3:消息内容 rabbitTemplate.convertAndSend(exchangeName,"",message); }都会收到:
(2)Direct交换机
1.创建队列direct.queue1和direct.queue2
2.声明交换机hmall.direct ,将两个队列与其绑定
3.在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2
//监听direct.queue1队列的消息 @RabbitListener(queues = "direct.queue1") public void listenDirectQueue1(String msg) { System.out.println("消费者1从direct.queue1队列中接收到消息:{"+msg+"}"); } //监听direct.queue2队列的消息 @RabbitListener(queues = "direct.queue2") public void listenDirectQueue2(String msg) { System.out.println("消费者2从direct.queue2队列中接收到消息:{"+msg+"}"); }4.在publisher中编写测试方法,利用不同的RoutingKey向hmall. direct发送消息
//发送消息到hmall.direct的交换机 @Test public void testDirectExchange(){ //交换机的名称 String exchangeName = "hmall.direct"; //消息内容 String message = "蓝色警报!"; //参数1:交换机名称 参数2:路由key 参数3:消息内容 rabbitTemplate.convertAndSend(exchangeName,"blue",message); message="红色警报!"; //参数1:交换机名称 参数2:路由key 参数3:消息内容 rabbitTemplate.convertAndSend(exchangeName,"red",message); }测试打印:
(3)Topic交换机
1.声明队列topic.queue1和topic.queue2
2. 声明交换机hmall.topic ,将两个队列与其绑定
3. 在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2
@RabbitListener(queues = "topic.queue1") public void listenTopicQueue1(String msg) { System.out.println("消费者1从topic.queue1队列中接收到消息:{"+msg+"}"); } //监听topic.queue2队列的消息 @RabbitListener(queues = "topic.queue2") public void listenTopicQueue2(String msg) { System.out.println("消费者2从topic.queue2队列中接收到消息:{"+msg+"}"); }4. 在publisher中编写测试方法,利用不同的RoutingKey向hmall. topic发送消息
//发送消息到hmall.topic的交换机 @Test public void testTopicExchange(){ //交换机的名称 String exchangeName = "hmall.topic"; //消息内容 String message = "---111---中国体育新闻"; //参数1:交换机名称 参数2:路由key 参数3:消息内容 rabbitTemplate.convertAndSend(exchangeName,"china.sport",message); message="---222---新闻"; //参数1:交换机名称 参数2:路由key 参数3:消息内容 rabbitTemplate.convertAndSend(exchangeName,"china.news",message); message="---333---时政新闻"; //参数1:交换机名称 参数2:路由key 参数3:消息内容 rabbitTemplate.convertAndSend(exchangeName,"politic.news",message); }运行打印:
描述下Direct交换机与Topic交换机的差异?
- Topic交换机接收的消息RoutingKey可以是多个单词,以. 分割
- Topic交换机与队列绑定时的bindingKey可以指定通配符
- #:代表0个或多个词
- *:代表1个词
4. 声明队列和交换机
(1)方式1
例如,在consumer中声明一个Fanout类型的交换机配置类,并且创建队列与其绑定:
@Configuration //定义交换机、队列和其绑定 public class FanoutConfig { //定义一个名字为hmall.fanout的fanout交换机 @Bean public FanoutExchange hmallFanoutExchange() { return new FanoutExchange("hmall.fanout"); } //定义一个名字为fanout.queue1的队列 @Bean public Queue fanoutQueue1() { return new Queue("fanout.queue1"); } //定义一个名字为fanout.queue2的队列 @Bean public Queue fanoutQueue2() { return new Queue("fanout.queue2"); } //将fanout.queue1绑定到hmall.fanout交换机 @Bean public Binding bindFanoutQueue1(Queue fanoutQueue1, FanoutExchange hmallFanoutExchange) { //绑定队列到交换机 return BindingBuilder.bind(fanoutQueue1).to(hmallFanoutExchange); } //将fanout.queue2绑定到hmall.fanout交换机 @Bean public Binding bindFanoutQueue2(Queue fanoutQueue2, FanoutExchange hmallFanoutExchange) { //绑定队列到交换机 return BindingBuilder.bind(fanoutQueue2).to(hmallFanoutExchange); } }
(2)方式2
SpringAMQP还提供了基于@RabbitListener注解来声明队列和交换机的方式:
/* 声明交换机与队列和其绑定 消费者1:监听direct.queue1,绑定hmall.direct,routingKey为blue和red */ @RabbitListener(bindings = @QueueBinding( //声明队列,durable为true表示持久化的队列 value = @Queue(value = "direct.queue1",durable = "true"), //声明交换机,type表示交换机类型 exchange = @Exchange(value = "hmall.direct",type = ExchangeTypes.DIRECT), //声明路由key key = {"blue","red"})) public void listenDirectQueue1(String msg) { System.out.println("消费者1从direct.queue1队列中接收到消息:{"+msg+"}"); } /* 声明交换机与队列和其绑定 消费者2:监听direct.queue2,绑定hmall.direct,routingKey为yellow和red */ @RabbitListener(bindings = @QueueBinding( //声明队列,durable为true表示持久化的队列 value = @Queue(value = "direct.queue2",durable = "true"), //声明交换机 exchange = @Exchange(value = "hmall.direct",type = ExchangeTypes.DIRECT), //声明路由key key = {"yellow","red"})) public void listenDirectQueue2(String msg) { System.out.println("消费者2从direct.queue2队列中接收到消息:{"+msg+"}"); }
5. 消息转换器
JDK序列化:
//接受object.queue队列的消息,如果队列不存在则创建 @RabbitListener(queues = "object.queue") public void listenObjectQueue(Map<String,Object> msg) { System.out.println("消费者从object.queue队列中接收到消息:"+msg); } //发送一个map数据结构的消息到object.queue队列 @Test public void testObjectQueue(){ //队列名称 String queueName = "object.queue"; //消息内容 Map<String,Object> message = new HashMap<>(); message.put("name","黑马"); message.put("age",18); //参数1:队列名称 参数2:消息内容 rabbitTemplate.convertAndSend(queueName,message); }控制台显示;
Spring的对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的。而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化。 存在下列问题:
- JDK的序列化有安全风险
- JDK序列化的消息太大
- JDK序列化的消息可读性差
消息转换器:
建议采用JSON序列化代替默认的JDK序列化,要做两件事情:
在publisher和consumer中都要引入jackson依赖:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>在publisher和consumer的启动类中都要配置MessageConverter:
//注册json消息转换器 @Bean public MessageConverter messageConverter() { Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); //设置每个消息都携带一个id converter.setCreateMessageIds(true); return converter; }重新启动测试:
四. 生产者的可靠性
1. 生产者重试
有的时候由于网络波动,可能会出现客户端连接MQ失败的情况。通过配置我们可以开启连接失败后的重试机制:
spring: rabbitmq: connection-timeout: 1s #设置MQ的连接超时时间 template: retry: enabled: true #开启超时重试机制 max-attempts: 3 #最大重试次数 initial-interval: 1s #初始等待时长 multiplier: 1 #每次重试的间隔时长倍数,每次等待时长=初始等待时长*倍数+超时时间当网络不稳定的时候,利用重试机制可以有效提高消息发送的成功率。不过SpringAMQP提供的重试机制是阻塞式的重试,也就是说多次重试等待的过程中,当前线程是被阻塞的,会影响业务性能。
如果对于业务性能有要求,建议禁用重试机制。如果一定要使用,请合理配置等待时长和重试次数,当然也可以考虑使用异步线程来执行发送消息的代码。
2. 生产者确认
1. 在publisher这个微服务的application.yml中添加配置:
spring: rabbitmq: publisher-confirm-type: correlated #消息发送确认模式,会严重影响性能 publisher-returns: true #开启消息发送失败返回,默认false,会严重影响性能配置说明:
这里publisher-confirm-type有三种模式可选:
unone:关闭confirm机制
usimple:同步阻塞等待MQ的回执消息
correlated:MQ异步回调方式返回回执消息
2.新增配置类,每个RabbitTemplate只能配置一个ReturnCallback,因此需要在publisher项目启动过程中配置:
@Configuration public class MqConfig { @Autowired private RabbitTemplate rabbitTemplate; //对rabbitTemplate设置一个return异常处理 @PostConstruct public void initRabbitTemplate(){ rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() { @Override public void returnedMessage(ReturnedMessage returned) { System.out.println("------ReturnsCallback 一般情况下:路由出现了问题才会执行的回调"); System.out.println("交换机:"+returned.getMessage()); System.out.println("路由健:"+returned.getRoutingKey()); System.out.println("消息:"+returned.getMessage()); System.out.println("replyCode:"+returned.getReplyCode()); System.out.println("replyText:"+returned.getReplyText()); System.out.println("-------ReturnsCallback end-------"); } }); } }添加测试类,故意向不存在的路由发信息:
//测试publisher return & confirm @Test public void testPublisherReturnAndConfirm(){ String message = "hello i am msg"; rabbitTemplate.convertAndSend("hmall.direct","xxx",message); }控制台打印:
3.发送消息,指定消息ID、消息ConfirmCallback
//测试publisher return & confirm @Test public void testPublisherReturnAndConfirm(){ //发送确定回调 CorrelationData correlationData = new CorrelationData(); correlationData.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() { @Override public void onFailure(Throwable ex) { //当MQ返回正常,但是在我们自己的应用服务器中接收到来自MQ的消息失败时才会触发执行,几乎不可能触发 System.out.println("发送消息失败,执行力onFailure"); } @Override public void onSuccess(CorrelationData.Confirm result) { //接收成功来自MQ的应答,但是并不一定是ack还是nack if (result.isAck()){ System.out.println("消息发送成功; onSuccess; 返回ack"); }else { System.out.println("消息发送失败,onSuccess;返回nack;失败的原因:"+result.getReason()); } } }); String message = "hello i am msg"; rabbitTemplate.convertAndSend("hmall.direct","xxx",message,correlationData); }测试:故意把路由写错
故意把交换机写错:
如何处理生产者的确认消息?
- 生产者确认需要额外的网络和系统资源开销,尽量不要使用
- 如果一定要使用,无需开启Publisher-Return机制,因为一般路由失败是自己业务问题
- 对于nack消息可以有限次数重试,依然失败则记录异常消息
五.MQ的可靠性
1. 数据持久化
RabbitMQ实现数据持久化包括3个方面:
1. 交换机持久化
2. 队列持久化
3.消息持久化
持久化的消息在没用被消费的情况下是存在的,消费了的话会被删除(也就是被队列接收了会被删除,没被接收不会删除)
出于性能考虑,为了减少IO次数,发送到MQ的消息并不是逐条持久化到硬盘的,而是每隔一段时间批量持久化。一般间隔在100毫秒左右,这就会导致ACK有一定的延迟,因此建议生产者确认全部采用异步方式。
在开启持久化机制以后,如果同时还开启了生产者确认,那么MQ会在消息持久化以后才发送ACK回执,进一步确保消息的可靠性。
如果队列是临时队列但是往它里面发了持久化消息,这时候重启MQ它的消息也不存在
2.LazyQueue
从RabbitMQ的3.6.0版本开始,就增加了Lazy Queue的概念,也就是惰性队列。
惰性队列的特征如下:
- 接收到消息后直接存入磁盘而非内存(内存中只保留最近的消息,默认2048条)
- 消费者要消费消息时才会从磁盘中读取并加载到内存
- 支持数百万条的消息存储
在3.12版本后,所有队列都是Lazy Queue模式,无法更改。
要设置一个队列为惰性队列,只需要在声明队列时,指定x-queue-mode属性为lazy即可:
代码配置Lazy模式(了解):
在利用SpringAMQP声明队列的时候,添加 x-queue-mod=lazy 参数也可设置队列为Lazy模式:
@Bean public Queue lazyQueue(){ return QueueBuilder.durable("lazy.queue") .lazy() //消息队列是否是lazy模式;里面设置了一个参数x-queue-mode, 表示是否是lazy模式 .build(); }也可以基于注解来声明队列并设置为Lazy模式:
@RabbitListener( queuesToDeclare = @Queue( name = "lazy.queue",durable = "true", arguments = @Argument(name = "x-queue-mode", value = "lazy"))) public void listenLazyQueue(String message) { System.out.println("lazyQueue: " + message); }
RabbitMQ如何保证消息的可靠性:
- 首先通过配置可以让交换机、队列、以及发送的消息都持久化。这样队列中的消息会持久化到磁盘,MQ重启消息依然存在。
- RabbitMQ在3.6版本引入了LazyQueue,并且在3.12版本后会称为队列的默认模式。LazyQueue会将所有消息都持久化。
- 开启持久化和生产者确认时, RabbitMQ只有在消息持久化完成后才会给生产者返回ACK回执
六. 消费者的可靠性
1.消费者确认机制
分别测试 acknowledge-mode的值:
- none -> 抛出MessageConversionException消息转换异常,将被reject
- auto -> 抛出业务异常;消息将被保留在队列
修改yml:
spring: rabbitmq: listener: direct: #消息确认模式 默认是自动确认auto,手动确认需要开启手动确认模式,none不确认直接删除 acknowledge-mode: none//接收simple.queue2队列的消息 @RabbitListener(queues = "simple.queue2") public void listenSimpleQueue2(String msg) { System.out.println("消费者从simple.queue2队列中接收到消息:"+msg); //消息抛出消息格式转换异常,这时会被reject,消息会被删除 //throw new MessageConversionException("抛出消息格式转换异常"); //故意抛出运行时异常,这时候返回nack,消息不会删除,会重新入队,再次接收到 //yml里需要把none修改为auto throw new RuntimeException("抛出业务异常"); }//发送simple.queue2队列的消息 @Test public void testSimpleQueue2() { //队列名称 String message = "hello,simple.queue2"; //参数1:队列名称 参数2:消息内容 rabbitTemplate.convertAndSend("simple.queue2", message); }
2. 失败重试机制
当消费者出现异常后,消息会不断requeue(重入队)到队列,再重新发送给消费者。如果消费者再次执行依然出错,消息会再次requeue到队列,再次投递,直到消息处理成功为止。会引起MQ服务器不必要压力。
为了应对不断重新入队的情况;Spring又提供了消费者失败重试机制:在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列。
修改consumer服务的application.yml文件,添加内容:
spring: rabbitmq: listener: retry: enabled: true #开启消费者重试机制 max-attempts: 3 #最大重试次数 initial-interval: 1000ms #初始等待时长 multiplier: 1 #每次重试的间隔时长倍数,每次等待时长=初始等待时长*倍数 stateless: true #状态模式,每次是否新连接投递,如果开启了消息事务则需要设置为false
3.消费失败处理
在之前的测试中,本地测试达到最大重试次数后,消息会被丢弃。这在某些对于消息可靠性要求较高的业务场景下,显然不太合适了。
比较优雅的一种处理方案是 RepublishMessageRecoverer ,失败后将消息投递到一个指定的,专门存放异常消息的队列,后续由人工集中处理。
采用第三种方式的话:在消费者方创建 com.itheima.consumer.config.ErrorMessageConfig 代码如下:
@Configuration //条件注解:当某个属性值条件成立时,当前配置文件才生效 @ConditionalOnProperty(name = "spring.rabbitmq.listener.simple.retry.enabled", havingValue = "true") public class ErrorMessageConfig { @Autowired private RabbitTemplate rabbitTemplate; //声明错误的消息路由交换机 @Bean public DirectExchange directExchange(){ return new DirectExchange("error.direct"); } //声明错误消息队列 @Bean public Queue errorQueue(){ return new Queue("error.queue", true); } //声明错误消息绑定上述的交换机和队列 @Bean public Binding errorBinding(DirectExchange directExchange, Queue errorQueue){ return BindingBuilder.bind(errorQueue).to(directExchange).with("error"); } //声明失败消息处理 @Bean public MessageRecoverer messageRecoverer(){ return new RepublishMessageRecoverer(rabbitTemplate,"error.direct","error"); } }启动consumer;然后再次发送消息到 simple.queue 可以看到在 error.queue 队列中有消息 及异常信息。
消费者如何保证消息一定被消费?
- 开启消费者确认机制为auto,由spring确认消息处理成功后返回ack,异常时返回nack
- 开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理
4.业务幂等性
给每个消息都设置一个唯一id,利用id区分是否是重复消息:
- ①每一条消息都生成一个唯一的id,与消息一起投递给消费者。
- ②消费者接收到消息后处理自己的业务,业务处理成功后将消息ID保存到数据库
- ③如果下次又收到相同消息,去数据库查询判断是否存在,存在则为重复消息放弃处理。
在配置类中写:
//注册json消息转换器 @Bean public MessageConverter messageConverter() { Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); //设置每个消息都携带一个id converter.setCreateMessageIds(true); return converter; }
七.死信交换机
1.延迟消息
2.死信交换机
实现步骤:
1、按照上图绑定交换机与队列;routingKey为空
2、编写发送到 simple.direct 延迟10s的消息 与接收 dlx.queue 消息的代码进行测试
注意:如果要使用死信交换机那就不能设置simple.queue2的监听,否则就不生效了。
消费者监听:
//接收dlx.queue队列的消息 @RabbitListener(queues = "dlx.queue") public void listenDlxQueue(String msg) { System.out.println(LocalTime.now()+" ---消费者从dlx.queue队列中接收到消息:"+msg); }发送者测试:
//发送延迟5秒的消息到simple.direct队列路由key为空 @Test public void testSimpleDirect() { //队列名称 String message = "hello,延迟消息"; System.out.println("发送时间:"+ LocalTime.now()); //参数1:交换机名称 参数2:路由key 参数3:消息内容 rabbitTemplate.convertAndSend("simple.direct", "", message, new MessagePostProcessor() { @Override public Message postProcessMessage(Message message) throws AmqpException{ //对消息设置过期时间5s message.getMessageProperties().setExpiration("5000"); return message; } }); }
3.延迟消息插件
RabbitMQ的官方也推出了一个插件,原生支持延迟消息功能。该插件的原理是设计了一种支持延迟消息功能的交换机,当消息投递到交换机后可以暂存一定时间,到期后再投递到队列。
官方文档说明:https://www.rabbitmq.com/blog/2015/04/16/scheduling-messages-with-rabbitmq
下载插件:https://www.rabbitmq.com/community-plugins
进入MQ的安装目录中的plugins目录,把插件放入到plugins目录
进入sbin目录,打开管理员控制台,执行以下命令:
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
重启MQ,查看延时队列:
如要通过插件实现延迟消息的话;需要在声明的时候;对交换机设置 delayed属性为true。
1. 注解方式:
/* 接收delay.queue 队列的消息 */ @RabbitListener(bindings = @QueueBinding( value = @Queue(value = "delay.queue", durable = "true"), //delayed="true"表示延迟交换机 exchange = @Exchange(value = "delay.direct",delayed = "true"), key = "delay")) public void listenDelayQueueMessage(String message) throws Exception { System.out.println("SpringRabbitListener listenDelayQueueMessage 消费者接收delay.queue的延迟消息: " + message + " " + LocalTime.now()); }发送延迟消息:
/** * 发送延迟5s的消息到delay.direct 路由key为delay */ @Test public void testDelayMessage() { // 发送 5s 过期消息 System.out.println("发送时间:" + LocalTime.now()); //参数1:交换机名称 参数2:路由key 参数3:消息内容 rabbitTemplate.convertAndSend("delay.direct", "delay", "hello, delay message", new MessagePostProcessor() { @Override public Message postProcessMessage(Message message) throws AmqpException { //对消息设置延迟时间 message.getMessageProperties().setDelay(5000); return message; } }); }










































































51万+

被折叠的 条评论
为什么被折叠?



