RabbitMQ基础教程,看完这篇就够用了

目录

一.同步异步调用

1. 同步调用

2.异步调用

二. MQ选型

1. MQ技术选型

2.RabbitMQ的介绍和安装

(1)简介

(2)安装

1️⃣Erlang安装:

2️⃣RabbitMQ安装

3.快速入门

4.数据隔离

三. Java客户端

1. 快速入门(简单模型)

2. Work Queues

3. fanout交换机

(1)Fanout广播

(2)Direct交换机

(3)Topic交换机 

4. 声明队列和交换机

(1)方式1

(2)方式2

5. 消息转换器

四. 生产者的可靠性

1. 生产者重试

2. 生产者确认

五.MQ的可靠性

1. 数据持久化

2.LazyQueue

六. 消费者的可靠性

1.消费者确认机制

2. 失败重试机制

3.消费失败处理

4.业务幂等性

七.死信交换机

1.延迟消息

2.死信交换机

3.延迟消息插件


一.同步异步调用

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的回执消息

     correlatedMQ异步回调方式返回回执消息

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

RabbitMQ3.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重启消息依然存在。
  • RabbitMQ3.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又提供了消费者失败重试机制:在消费者出现异常时利用本地重试,而不是无限制的requeuemq队列。

修改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;
       }
    });
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端小马

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值