异步调用的优势:

  • 解除耦合,扩展性强,
  • 异步调用,无需等待,性能好
  • 故障隔离,下游故障不影响上游业务
  • 缓存消息,流量削峰填谷

异步调用的问题

  • 不能立刻得到结果,时效性差
  • 不能确定下游业务是否执行成功
  • 业务安全依赖于Broker(代理)的可靠性

docker安装启动rabbitmq

https://blog.csdn.net/a3562323/article/details/104222229

RabbitMQ_springboot

因为docker启动rabbitmq的时候,没有设置用户名密码,所以默认都是guest

RabbitMQ_rabbitmq_02

RabbitMQ_springboot_03

控制台测试

  • 手动创建队列 在这里创建了两个,一个叫test.queues1,一个叫test.queues2
  • 交换机绑定队列 先选择fanout,广播消息,选择交换机后,要先给交换机绑定队列,不然的话,消息不知道要发送到哪里, 在队列中也可以看到,队列绑定了对应的交换机
  • 发送消息
  • 查看消息 如果遇到只有Overview,没有Messages的情况,需要去修改rabbitmq的配置文件,/etc/rabbitmq/conf.d/management_agent.disable_metrics_collector.conf,management_agent.disable_metrics_collector.conf中的属性true改为false,然后重启rabbitmq,就有messages了

在Get messages中查看队列接受的消息,这里只是查看消息内容,并没有消费掉

RabbitMQ_rabbitmq_04

数据隔离


  • 新建用户
  • RabbitMQ_rabbitmq_05

  • 创建成功,但是显示没有任何虚拟主机
  • RabbitMQ_springboot_06

  • 切换成刚创建的用户,然后看看消息
  • RabbitMQ_rabbitmq_07

  • 显示被拒绝了。能看到是因为这个账户是管理员,看不到消息是因为虚拟主机不同。

  • 为用户创建虚拟主机
  • RabbitMQ_spring_08


  • 测试不同虚拟主机之间数据隔离
  • 创建好虚拟主机以后,切换成自己的虚拟主机,对应的队列也会切换成对应的虚拟主机的队列,因为这个虚拟主机队列是空的,所以这里队列也是空的
  • RabbitMQ_spring_09

AMQP和Spring AMQP

AMQP:消息通信协议,与语言和平台无关。 Spring AMQP:基于AMQP协议定义的一套API规范,提供了模板发送和接收消息。

java+RabbitMq

目录结构

RabbitMQ_springboot_10

引入依赖

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-amqp</artifactId>
 </dependency>

配置文件

配置ip,端口,虚拟空间,用户名,密码

spring:
  rabbitmq:
    host: 192.168.1.14
    port: 5672
    virtual-host: /z
    username: zhangsan
    password: 123

消息发送

package com.publisher;


import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class PublisherApplicationTests {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendMessage(){
        String queueName = "zhang.queues1";
        String message = "Hello Java Message";

        rabbitTemplate.convertAndSend(queueName, message);
    }

}

RabbitMQ_spring_11

消息接收

编写rabbitmq的监听类

package com.consumer.listeners;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class MqListener {

    @RabbitListener(queues = "zhang.queues1")
    public void listenSimpleQueue(String msg){
        System.out.println("消费者消费消息:"+msg);

    }
}

启动consumer的服务,启动成功后,显示消费了消息

RabbitMQ_springboot_12

查看rabbitmq的控制台,消息已经被消费了

RabbitMQ_rabbitmq_13

多次测试,只要消费者在线,发送者一发送,消息就会被消费。

RabbitMQ_spring_14

队列

Work Queues

一个队列绑定多个消费者,可以提高消息处理速度,每个消息只能被消费一次,但是每个消费者通过轮询的方式进行消费。

  • 创建新的队列测试
  • 修改消费者监听代码,创建两个监听,监听刚创建的队列work.queue,启动消费者
package com.consumer.listeners;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class MqListener {

    @RabbitListener(queues = "zhang.queues1")
    public void listenSimpleQueue(String msg){
        System.out.println("消费者消费消息:"+msg);

    }

    @RabbitListener(queues = "work.queue")
    public void listenWorkQueue1(String msg){
        System.out.println("消费者1消费消息-------------:"+msg);

    }

    @RabbitListener(queues = "work.queue")
    public void listenWorkQueue2(String msg){
        System.out.println("消费者2消费消息=============:"+msg);

    }
}
  • 发送消息,发送50条,查看效果
package com.publisher;


import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class PublisherApplicationTests {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendMessage(){
        String queueName = "zhang.queues1";
        String message = "Hello Java Message";

        rabbitTemplate.convertAndSend(queueName, message);
    }

    @Test
    public void testWorkQueue() throws InterruptedException {
        String queueName = "work.queue";

        for (int i = 0; i < 50; i++) {
            String message = "Hello Java Message"+i;
            rabbitTemplate.convertAndSend(queueName, message);
            Thread.sleep(20);
        }
    }

}

通过观察,是轮询的机制,一个监听消费一条消息

RabbitMQ_springboot_15

  • 调整预取数量,控制它的消费方式配置文件
spring:
  rabbitmq:
    host: 192.168.1.14
    port: 5672
    virtual-host: /z
    username: zhangsan
    password: 123
    listener:
      simple:
        prefetch: 1    //预取数量设置为1,先消费完的先处理消息

监听方法

@RabbitListener(queues = "work.queue")
    public void listenWorkQueue1(String msg) throws InterruptedException {
        System.out.println("消费者1消费消息-------------:"+msg);
        //如果不配置预取数量,就算这里睡眠了,还是会发现,通过轮询来消费的
        Thread.sleep(200);
    }

    @RabbitListener(queues = "work.queue")
    public void listenWorkQueue2(String msg){
        System.out.println("消费者2消费消息=============:"+msg);

    }

可以看到现在不是轮询了

RabbitMQ_spring_16

交换机

真实环境中都是通过交换机发送消息,而不是直接通过队列,交换机有以下三种:

  • Fanout:广播
  • Direct:定向
  • Topic:话题

Fanout交换机

Fanout Exchange会将接收到的消息广播到每一个跟它绑定的队列,所以也叫广播模式

RabbitMQ_springboot_17


  • 控制台创建队列fanout.queue1fanout.queue2
  • RabbitMQ_spring_18


  • 控制台选择交换机,并绑定刚才创建的队列fanout.queue1fanout.queue2
  • RabbitMQ_spring_19

  • 编写消费者,监听刚才创建的两个队列,然后启动消费者
package com.consumer.listeners;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class MqListener {

    @RabbitListener(queues = "fanout.queue1")
    public void listenFanoutQueue1(String msg) throws InterruptedException {
        System.out.println("消费者1消费消息,fanout.queue1-------------:"+msg);
    }

    @RabbitListener(queues = "fanout.queue2")
    public void listenFanoutQueue2(String msg){
        System.out.println("消费者2消费消息,fanout.queue2=============:"+msg);

    }
}
  • 编写发送者,发送消息到前面绑定好队列的交换机,
@Test
    public void testFanoutMessage(){
        String exchangeName = "amq.fanout";
        String message = "Hello Java Message";

        rabbitTemplate.convertAndSend(exchangeName,null, message);
    }

因为已经绑定了队列到交换机,当有消息进入交换机,交换机会将消息路由到每个绑定的队列

RabbitMQ_spring_20

Direct交换机

Direct Exchange会将接收到的消息根据规则路由到指定的Queue,因此称为定向路由。

  • 控制台创建两个队列direct.queue1direct.queue2
  • 控制台绑定队列到交换机amq.direct,需要注意的是,我们这里做的是Direct交换机,所以是有路由键的
  • 消费者代码,监听刚才创建的队列,direct.queue1direct.queue2,然后启动消费者
package com.consumer.listeners;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class MqListener {

    @RabbitListener(queues = "direct.queue1")
    public void listenDirectQueue1(String msg){
        System.out.println("消费者1消费消息,direct.queue1-------------:"+msg);
    }

    @RabbitListener(queues = "direct.queue2")
    public void listenDirectQueue2(String msg){
        System.out.println("消费者2消费消息,direct.queue2=============:"+msg);

    }
}
  • 发送者代码
@Test
    public void testDirectMessage(){
        String exchangeName = "amq.direct";
        String message = "Hello Java Message  Red";

        rabbitTemplate.convertAndSend(exchangeName,"red", message);

        message = "Hello Java Message  blue";

        rabbitTemplate.convertAndSend(exchangeName,"blue", message);

        message = "Hello Java Message  yellow";

        rabbitTemplate.convertAndSend(exchangeName,"yellow", message);
    }
  • 结果

Topic交换机

Topic Echange和DirectExchange类似,却别在于routingKey可以是多个单词列表,并且以.分割。

RabbitMQ_spring_21

  • 创建两个队列topic.queue1topic.queue2
  • 队列与topic交换机绑定
  • 消费者监听topic的两个队列
package com.consumer.listeners;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class MqListener {

    @RabbitListener(queues = "topic.queue1")
    public void listenTopicQueue1(String msg){
        System.out.println("消费者1消费消息,topic.queue1-------------:"+msg);
    }

    @RabbitListener(queues = "topic.queue2")
    public void listenTopicQueue2(String msg){
        System.out.println("消费者2消费消息,topic.queue2=============:"+msg);

    }
}
  • 发送者发送消息
@Test
    public void testTopicMessage(){
        String exchangeName = "amq.topic";
        String message = "Hello Java Message ";

        rabbitTemplate.convertAndSend(exchangeName,"japan.news", message);


    }

只匹配了这一个路由

RabbitMQ_springboot_22

所以结果,只有消费者2收到

RabbitMQ_spring_23

改一下,

@Test
    public void testTopicMessage(){
        String exchangeName = "amq.topic";
        String message = "Hello Java Message ";

        rabbitTemplate.convertAndSend(exchangeName,"china.news", message);

    }

两个路由都匹配到了

RabbitMQ_springboot_24

所以结果

RabbitMQ_spring_25

java声明交换机和队列

之前为了测试入手,都使用的控制台创建的队列以及交换机,这样的效率无疑是很低的

RabbitMQ_springboot_26

fanout交换机

配置配置类,然后启动消费者

package com.consumer.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FanoutConfiguration {

	//第一种,绑定时候通过入参绑定

    @Bean
    public FanoutExchange fanoutExchange(){
        //Exchange build = ExchangeBuilder.fanoutExchange("zs.fanout").build();
        return new FanoutExchange("java.fanout");
    }

    @Bean
    public Queue fanoutQueue(){
        //Queue queue = QueueBuilder.durable("java.fanout.queue").build();//持久化
        return new Queue("java.fanout.queue");
    }

    @Bean
    public Binding fanoutBinding(Queue fanoutQueue,FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue).to(fanoutExchange);
    }

	//第二种,绑定时候通过方法名绑定

    @Bean
    public FanoutExchange fanoutExchange1(){
        //Exchange build = ExchangeBuilder.fanoutExchange("zs.fanout").build();
        return new FanoutExchange("java.fanout1");
    }

    @Bean
    public Queue fanoutQueue1(){
        //Queue queue = QueueBuilder.durable("java.fanout.queue").build();//持久化
        return new Queue("java.fanout.queue1");
    }

    @Bean
    public Binding fanoutBinding1(){
        //fanoutQueue1()这种不是直接调用当前类,当前类是个配置类,并且对象都是@Bean修饰,都是从spring中去取的
        return BindingBuilder.bind(fanoutQueue1()).to(fanoutExchange1());
    }

}

RabbitMQ_springboot_27

direct交换机

设置配置类,然后启动消费者

package com.consumer.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DirectConfiguration {

//创建交换机
    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange("java.direct");
    }
//创建队列
    @Bean
    public Queue directQueue1(){
        return new Queue("java.direct.queue1");
    }
//绑定队列到交换机,并且设置路由键
    @Bean
    public Binding directQueue1BindingRed(DirectExchange directExchange,Queue directQueue1){
        return BindingBuilder.bind(directQueue1).to(directExchange).with("red");
    }
//绑定队列到交换机,并且设置路由键
    @Bean
    public Binding directQueue1BindingBlue(DirectExchange directExchange,Queue directQueue1){
        return BindingBuilder.bind(directQueue1).to(directExchange).with("blue");
    }
//创建队列2
    @Bean
    public Queue directQueue2(){
        return new Queue("java.direct.queue2");
    }
//绑定队列2到交换机,并绑定路由键
    @Bean
    public Binding directQueue2BindingRed(DirectExchange directExchange,Queue directQueue2){
        return BindingBuilder.bind(directQueue2).to(directExchange).with("red");
    }
//绑定队列2到交换机,并绑定路由键
    @Bean
    public Binding directQueue2BindingBlue(DirectExchange directExchange,Queue directQueue2){
        return BindingBuilder.bind(directQueue2).to(directExchange).with("yellow");
    }
}

RabbitMQ_spring_28

基于注解的声明交换机和队列

之前有用过一个注解@RabbitListener,之前只是用它来声明队列的名称,其实也可以绑定对应的交换机和队列现在删除之前通过@Bean创建的队列和交换机,然后注释掉之前的两个配置类,然后再来测试

  • 编写监听类
package com.consumer.listeners;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class MqListener {

	//绑定队列java.direct.queue1到交换机java.direct,交换机类型是ExchangeTypes.DIRECT,路由key是red和blue
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name="java.direct.queue1",durable = "true"),
            exchange = @Exchange(name="java.direct",type = ExchangeTypes.DIRECT),
            key={"red","blue"}
    ))
    public void javaListenDirectQueue1(String msg){
        System.out.println("消费者1消费了消息:"+msg);
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name="java.direct.queue2",durable = "true"),
            exchange = @Exchange(name="java.direct",type = ExchangeTypes.DIRECT),
            key={"red","yellow"}
    ))
    public void javaListenDirectQueue2(String msg){
        System.out.println("消费者2消费了消息:"+msg);
    }
}
  • 发送消息
@Test
    public void testDirectMessage(){
        String exchangeName = "java.direct";
        String message = "Hello Java Message  Red";

        rabbitTemplate.convertAndSend(exchangeName,"red", message);

        message = "Hello Java Message  blue";

        rabbitTemplate.convertAndSend(exchangeName,"blue", message);

        message = "Hello Java Message  yellow";

        rabbitTemplate.convertAndSend(exchangeName,"yellow", message);
    }

结果:

RabbitMQ_springboot_29

消息转换器

发送Map结构的数据

  • 先创建一个测试用的queue
  • 发送消息
@Test
    public void testObject(){
        Map<String, Object> object = new HashMap<String, Object>();
        object.put("name","zhangsan");
        object.put("age",17);
        rabbitTemplate.convertAndSend("object.queue",object);
    }
  • 观察结果 被序列化了。 然后重新发送消息,查看结果
@Test
    public void testObject(){
        Map<String, Object> object = new HashMap<String, Object>();
        object.put("name","zhangsan");
        object.put("age",17);
        rabbitTemplate.convertAndSend("object.queue",object);
    }

RabbitMQ_spring_30

  • 消费者消费消息
@RabbitListener(queues = "object.queue")
    public void objectQueue(Map<String,Object> map){
        System.out.println(map);
    }
  • 发送者发送消息
@Test
    public void testObject(){
        Map<String, Object> object = new HashMap<String, Object>();
        object.put("name","zhangsan");
        object.put("age",17);
        rabbitTemplate.convertAndSend("object.queue",object);
    }

RabbitMQ_springboot_31

业务改造

  • 消费者,trade-service服务原本直接调用接口,标记订单已支付,现在进行改造。接口代码不动。
@ApiOperation("标记订单已支付")
@ApiImplicitParam(name="orderId",value="订单id",paramType="path")
@PutMapping("/{orderId}")
public void markOrderPaySucess(@PathVariable("orderId")Long orderId){
	orderService.markOrderPaySucess(orderId);
}

添加rabbitmq的配置信息到配置文件,然后新增rabbitmq监听类

@Component
@RequiredArgsConstructor
public class PayStatusListener{
	private final IOrderService orderService;
	@RabbitListener(bindings=@QueueBinding(
		value=@Queue(name="mark.order.pay.queue",durable="true"),
		exchange=@Exchange(name="pay.topic",type=ExchangeTypes.TOPIC),
		key="pay.success"
	))
	public void listenerOrderPay(Long orderId){
		//标记订单状态已支付
		orderService.markOrderPaySucess(orderId);
	}
}
  • 发送者将原本调用trade-service服务,标记订单已支付的方法进行修改,不直接进行调用借口了,而是向队列发送消息。
//修改订单状态
//tradeClient.markOrderPaySucess(po.getBizOrderNo());
rebbitTemplate.converAndSend("pay.topic","pay.success",po.getBizOrderNo());

对应的队列消费者监听到有消息进来后,就会处理修改订单的支付状态

消息的可靠性

  1. 消息发送的时候丢失了
  2. mq把消息丢了
  3. 消费者把消息丢了消息进入mq,mq突然挂掉,或者交易服务突然挂掉。

生产者的可靠性

生产者重连

需要注意的是,这个生产者重连,会阻塞线程

RabbitMQ_spring_32

  • 测试发送者发送失败打开连接失败的重试,停掉rabbitmq服务

生产者确认

RabbitMQ_rabbitmq_33

RabbitMQ_spring_34

RabbitMQ_rabbitmq_35

RabbitMQ_rabbitmq_36

  • 修改配置文件
  • 编写配置类
package com.publisher.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class MqConfirmConfig implements ApplicationContextAware {


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        //配置回调
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            @Override
            public void returnedMessage(ReturnedMessage returnedMessage) {
                log.debug("收到消息得到return callback,exchange:{},routingkey:{},message:{},replycode:{},replytext:{}",returnedMessage.getExchange(),returnedMessage.getRoutingKey(),returnedMessage.getMessage(),returnedMessage.getReplyCode(),returnedMessage.getReplyText());
            }
        });
    }
}
  • 生产者的确认,在每次发送都得配置
@Test
    public void testConfirmCallBack() throws InterruptedException {
        //创建cd
        CorrelationData cd = new CorrelationData(UUID.randomUUID().toString());
        //添加confirmcallback
        cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
            @Override
            public void onFailure(Throwable ex) {
                log.error("消息回调失败");
            }

            @Override
            public void onSuccess(CorrelationData.Confirm result) {
                log.debug("收到confirm callback回调");
                if(result.isAck()){
                    //消息发送成功
                    log.debug("消息发送成功,收到ACK");
                }else{
                    //消息发送失败
                    log.debug("消息发送成功,收到NACK,原因:{}",result.getReason());
                }
            }
        });

        rabbitTemplate.convertAndSend("java.direct","red","hello",cd);

        Thread.sleep(2000);
    }

RabbitMQ_rabbitmq_37

MQ的可靠性

RabbitMQ_spring_38

数据持久化

RabbitMq实现数据持久化的三个方面:

  • 交换机持久化
  • 队列持久化
  • 消息持久化

Lazy Queue

RabbitMQ_rabbitmq_39

  • 控制台创建时
  • 代码创建时
  • 注解创建时

消费者的可靠性

消费者确认机制

RabbitMQ_rabbitmq_40

RabbitMQ_springboot_41

  • 配置文件配置好自动模式,消费者监听这里故意抛出异常,然后debug卡在抛出异常这里
@RabbitListener(queues = "ack.queue")
    public void ackQueue(String msg){
        System.out.println("----------------"+msg);
        throw  new RuntimeException("异常处理");
    }

RabbitMQ_spring_42

  • 然后发送消息
@Test
    public void testAck(){
        String queue = "ack.queue";
        String msg="asdaaaa";
        rabbitTemplate.convertAndSend(queue,msg);
    }
  • 消息显示未返回结果
  • 因为在消费者监听那里抛出异常,然后消息就会一直重发,所以一直都卡在抛出异常的位置
  • 换一个消息转换异常抛出,就会删除消息
@RabbitListener(queues = "ack.queue")
    public void ackQueue(String msg){
        System.out.println("----------------"+msg);
        throw  new MessageConversionException("异常处理");
    }

RabbitMQ_springboot_43

RabbitMQ_rabbitmq_44

消费失败处理

RabbitMQ_rabbitmq_45

RabbitMQ_springboot_46

RabbitMQ_rabbitmq_47

  • 编写消费者的配置类
package com.consumer.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.retry.MessageRecoverer;
import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

/**
 * 重试机制开启的时候,才配置这个策略
 */
@Configuration
@ConditionalOnProperty(prefix = "spring.rabbitmq.listener.simple.retry",name = "enabled",havingValue = "true")      //这个配置类在属性spring.rabbitmq.listener.simple.retry。enabled为true的时候生效
public class ErrorConfiguration {

	//配置交换机

    @Bean
    public DirectExchange directExchange(){
        return new DirectExchange("error.direct");
    }
	//配置队列

    @Bean
    public Queue errorQueue(){
        return new Queue("error.queue");
    }

	//绑定队列到交换机,并指定路由
    @Bean
    public Binding errorBinding(DirectExchange directExchange,Queue errorQueue){
        return BindingBuilder.bind(errorQueue).to(directExchange).with("error");
    }

    @Bean
    public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate){
        return new RepublishMessageRecoverer(rabbitTemplate,"error.direct","error");
    }
}
  • 监听这里故意报错
@RabbitListener(queues = "zhang.queues1")
    public void listenSimpleQueue(String msg){
        System.out.println("消费者消费消息:"+msg);
        throw new RuntimeException("111111");
    }
  • 发送请求
@Test
    public void test1(){
        String queue = "zhang.queues1";
        String msg="asdaaaa";
        rabbitTemplate.convertAndSend(queue,msg);
    }
  • 结果,重试了三次,发送消息到指定的错误的交换机了
  • 查看控制台

业务幂等性

RabbitMQ_spring_48

RabbitMQ_rabbitmq_49

  • 配置一下创建消息id
@SpringBootApplication
public class ConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }


    @Bean
    public MessageConverter jacksonMessageConverter() {

        Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
        jackson2JsonMessageConverter.setCreateMessageIds(true);
        return jackson2JsonMessageConverter;
    }

}
  • 配置发送者创建消息id
@SpringBootApplication
public class PublisherApplication {

    public static void main(String[] args) {
        SpringApplication.run(PublisherApplication.class, args);
    }

    @Bean
    public MessageConverter jacksonMessageConverter() {

        Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
        jackson2JsonMessageConverter.setCreateMessageIds(true);
        return jackson2JsonMessageConverter;
    }

}
  • 看一下没消息id的效果
  • 配置消息id后的效果
  • 修改前面的业务代码,先获取对应的订单的状态
@Component
@RequiredArgsConstructor
public class PayStatusListener{
	private final IOrderService orderService;
	@RabbitListener(bindings=@QueueBinding(
		value=@Queue(name="mark.order.pay.queue",durable="true"),
		exchange=@Exchange(name="pay.topic",type=ExchangeTypes.TOPIC),
		key="pay.success"
	))
	public void listenerOrderPay(Long orderId){
		//查询订单
		Order order=oderService.getById(orderId);
		//判断订单状态是否未支付
		if(null==order || order.getStatus() != 1){
			//订单不存在或者状态异常
			return;
		}
		//标记订单状态已支付
		orderService.markOrderPaySucess(orderId);
	}
}

上面的代码可能涉及到多线程安全问题,也可以利用数据库的乐观锁来实现,这条sql不管执行多少次,不满足条件,都不会执行,也保证了幂等性

//update order set status=2 where id=? and status=1
orderService.lambdaUpdate()
	.set(Order::getStatus,2)
	.set(Order::getPayTime,LocalDateTime.now())
	.eq(Order::getId,orderId)
	.eq(Order::getStatus,1)
	.update();

RabbitMQ_spring_50

延时消息

RabbitMQ_springboot_51

死信交换机

RabbitMQ_spring_52

  • 控制台创建一个队列simple.queue,创建队列的时候,记得指定死信交换机, 然后创建交换机simple.direct,队列绑定到交换机,路由是hi,但是不绑定消费者,进入队列的消息就会变成死信
  • 然后创建一个死信队列dlx.queue,然后再创建一个死信交换机dlx.direct,队列绑定好交换机
  • 消费者监听死信队列
@RabbitListener(queues = "dlx.queue")
    public void listenerDlxQueue(String msg){
        log.info("dlx.queue收到了消息----------------"+msg);
    }
  • 发送一条消息到未绑定消息者的交换机,设置它的死信时间10s
@Test
    public void ackQueue(){

        rabbitTemplate.convertAndSend("simple.direct", "hi", "hello", new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                message.getMessageProperties().setExpiration("10000");
                return message;
            }
        });

        log.info("消息发送成功");
    }

延迟消息插件

RabbitMQ_spring_53

下载对应rabbitmq版本的插件:https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/tag/v3.13.0

下载以后放在docker容器中的rabbitmq的插件目录

docker exec -it rabbitmq bash

RabbitMQ_spring_54

然后把下载好的rabbitmq_delayed_message_exchange-3.13.0.ez,放到插件目录

docker cp rabbitmq_delayed_message_exchange-3.13.0.ez rabbitmq:/plugins

然后进入容器中的rabbitmq的插件目录后,启用插件

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

启用成功

RabbitMQ_spring_55

通过设置setDelay来延迟消息

RabbitMQ_spring_56

-消费者注册监听 绑定队列delay.queu到交换机delay.direct

@RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "delay.queue",durable = "true"),	
            exchange = @Exchange(value = "delay.direct",delayed = "true"),
            key = "hi"
    ))
    public void listenDelayQueue(String msg){
        log.info("接收到delay.queue的消息:{}",msg);
    }
  • 启动消费者,队列和交换机都成功创建
  • 发送消息
@Test
    public void ackQueue(){

        rabbitTemplate.convertAndSend("simple.direct", "hi", "hello", new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                message.getMessageProperties().setExpiration("10000");			//延迟10s发送
                return message;
            }
        });

        log.info("消息发送成功");
    }
  • 发送消费都成功,14秒发送,延时10秒,24秒收到

取消超时订单

RabbitMQ_rabbitmq_57

RabbitMQ_rabbitmq_58

资料来源:https://www.bilibili.com/video/BV1mN4y1Z7t9?p=1&vd_source=a72a5eec20ffdda34c9612310392f433