06 RabbitMQ

1. RabbitMQ中的消费方式

1.1 Hello-World 简单模式

一个生产者,一个默认的交换机,一个队列,一个消费者

06 RabbitMQ_第1张图片

1. 2 Work Queue 工作队列模式

一个生产者,一个默认的交换机,一个队列,多个消费者

由于只有一个队列,且一个队列中的消息只能被一个消费者消费,会造成消息竞争

应用场景

  1. 抢红包
  2. 资源任务调度

06 RabbitMQ_第2张图片

1.3 Publish/subscribe 发布订阅模式

一个生产者,一个Fanout类型交换机,多个队列,每个队列绑定一个消费者

Fanout类型的交换机会把每条消息都发布到每个队列中,每个队列可以有一个消费者接收消息进行消费逻辑

应用场景:发布广告

06 RabbitMQ_第3张图片

1.4 Routing 路由模式

一个生产者,一个DIRECT类型交换机,根据消息的规则发布到不同的队列,每个队列绑定一个消费者

生产者在创建DIRECT类型的exchange后,根据RoutingKey去绑定相应的队列,并且在发送消息时,指定消息的具体RoutingKey即可

应用场景: 根据生产者的要求发送给特定的一个或者一批队列发送信息。

06 RabbitMQ_第4张图片

1.5 Topic 通配符模式

一个生产者,一个Topic类型交换机,根据通配符的规则,发布到不同的队列,每个队列绑定一个消费者

生产者创建Topic的exchange并且绑定到队列中,这次绑定可以通过*和#关键字,对指定RoutingKey内容,编写时注意格式 xxx.xxx.xxx去编写, * 代表一个单词,而# 代表0或者多个单词

06 RabbitMQ_第5张图片

2. 消费者拒绝消息拒绝策略

消费者接收到消息进行处理后,有可能失败,失败后消费者可以拒绝该条消息,可以选择拒绝多条或者拒绝一条,可以选择销毁消息或者是重新放回队列

  1. channel.basicReject:只支持对一条消息进行拒绝

    void basicReject(long deliveryTag, boolean requeue) throws IOException;
    

    deliveryTag:消息的投递标签,每一条消息在该channel内都有且是唯一的,类似于id

    requeue:是否重新放入队列,false:直接销毁;true:重新放入队列

  2. channel.basicNack是 channel.basicReject的补充,提供一次对多条消息进行拒绝的功能

    void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException; 
    

    multiple:批量确认标志,false:只拒绝本消息;true:所有比该deliveryTag小的消息都会被拒绝,已经ack的除外

除此之外需要注意的是,如果队列只有一名消费者,那么requeue之后需要防止出现死循环!!!

3. 如何保证消息的可靠性、消息的持久化

消息传递的可靠性

  1. 生产者发布消息时,由于网络问题导致消息没有到交换机:事务和confirm机制
  2. 消息到达了交换机,但是RabbitMQ宕机了怎么办:队列持久化机制(交换机不能持久化)
  3. 消费者消费到一半,消费者宕机了怎么办:手动ack确认

06 RabbitMQ_第6张图片

3.1 生产者保证消息可靠投递

生产者要确定消息能够通过交换机到达到队列中

RabbitMq的事务:事务可以保证消息100%传递,可以通过事务的回滚去记录日志,然后定时再次发送当前消息;但是事务非常影响效率,因此RabbitMQ还提供了Confirm的确认机制

1. 普通confirm机制

三步走:1. 开启confirm, 2. 发送消息, 3. 判断操作是否成功

//3.1 开启confirm
channel.confirmSelect();
//3.2 发送消息
String msg = "Hello-World!";
channel.basicPublish("","HelloWorld",null,msg.getBytes());
//3.3 判断消息发送是否成功
if(channel.waitForConfirms()){
    System.out.println("消息发送成功");
}else{
    System.out.println("发送消息失败");
}

2. 批量confirm

同样三步走:1. 开启confirm, 2. 批量发送消息, 3. 判断是否发送到交换机;如果有一个失败就全部失败

//3.1 开启confirm
channel.confirmSelect();
//3.2 批量发送消息
for (int i = 0; i < 1000; i++) {
    String msg = "Hello-World!" + i;
    channel.basicPublish("","HelloWorld",null,msg.getBytes());
}
//3.3 确定批量操作是否成功
channel.waitForConfirmsOrDie();     // 当你发送的全部消息,有一个失败的时候,就直接全部失败 抛出异常IOException

3. 异步confirm

同样三步走:1. 开启confirm, 2. 批量发送消息, 3. 开始异步回调;重写发布成功和发布失败的方法

//3.1 开启confirm
channel.confirmSelect();
//3.2 批量发送消息
for (int i = 0; i < 1000; i++) {
    String msg = "Hello-World!" + i;
    channel.basicPublish("","HelloWorld",null,msg.getBytes());
}
//3.3 开启异步回调
channel.addConfirmListener(new ConfirmListener() {

    @Override
    public void handleAck(long deliveryTag, boolean multiple) throws IOException {
        System.out.println("消息发送成功,标识:" + deliveryTag + ",是否是批量" + multiple);
    }

    @Override
    public void handleNack(long deliveryTag, boolean multiple) throws IOException {
        System.out.println("消息发送失败,标识:" + deliveryTag + ",是否是批量" + multiple);
    }
});

4. return机制

三种confirm机制都是保证消息能够到达交换机,有可能队列名称、路由错了,到不了队列中;因此可以使用Return机制来监听消息是否到达指定队列中

开启Return机制,并在发送消息时,指定mandatory为true

// 开启return机制
channel.addReturnListener(new ReturnListener() {
    @Override
    public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
        // 当消息没有送达到queue时,才会执行。
        System.out.println(new String(body,"UTF-8") + "没有送达到Queue中!!");
    }
});

// 在发送消息时,指定mandatory参数为true
channel.basicPublish("","HelloWorld",true,null,msg.getBytes());
spring:
  rabbitmq:
    publisher-confirm-type: simple
    publisher-returns: true

3.2 消费者保证消息可靠消费(丢失、重复消费)

1. 开启手动确认消息

关闭自动确认,消费者在消费完消息后必须手动ACK

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual # 手动的

2. 为了避免队列因为没有收到ACK而一直重复投递造成循环;开启并配置重试次数

spring:
  rabbitmq:
    listener:
      simple:
      	acknowledge-mode: manual # 手动的
        retry:
          enabled: true # 开启消费者进行重试
          max-attempts: 5 # 最大重试次数
          initial-interval: 3000 # 重试时间间隔

3. 为了避免消息多次投递造成不幂等:使用Redis实现

在消费者消费消息之前,先将消息的id放到Redis中;

id-0:表示正在执行业务;id-1表示执行业务成功

在rabbitMQ将消息发送给消费者时:

  1. 先setnx
  2. 如果key不存在,那么就直接执行;
  3. 如果key已存在(setnx返回值1表示成功,返回0表示已存在):获取key的值,如果是0,那么消费者什么都不做;如果是1,直接手动ack

3.3 持久化

为了避免rabbitMQ宕机丢失消息,可以开启RabbitMQ的持久化

1.交换机持久化 在声明时指定durable为true
2.队列持久化 在声明时指定durable为true
3.消息持久化 在声明时指定delivery_mode为2(在BasicProperties props中)

4. 消息的积压

创建队列的时候,使用OverFlow来配置队列的溢出操作

  1. 默认值是drop-head将头部(最老的)消息丢弃或者变成死信
  2. 如果设置为reject-publish就是拒绝接收发布者的消息;同时如果开启了confirm生产者确认模式,会调用basicNack()方法通知发送者消息被拒绝

需要注意的是,如果消息被路由到多个队列,那么就算信道通知生产者消息被拒绝,但是还是会路由到别的可以接受的队列

5. TTL、死信队列是怎么实现的?延迟消息队列是怎么实现的?

5.1 TTL

TTL指的是Time To Live,存活时间

如果队列中的消息指定了存活时间,过期了之后没有被消费,要么直接丢弃,要么放入死信队列

指定存活时间两种方式:

  1. 给单个消息设置
  2. 给队列设置,队列中的所有消息都有相同的过期时间

应用场景:下单后,半个小时未支付后自动删除

给单个消息设置过期时间

		MessageProperties messageProperties=new MessageProperties();   // 消息属性对象
        messageProperties.setMessageId(UUID.randomUUID().toString());
        messageProperties.setExpiration("10000");  // 设置消息的过期时间为10秒
        Message message = new Message(msg.getBytes(),messageProperties);

给队列设置过期时间

 	@Bean
    public Queue ttlDirectQueue(){
        // 在队列上 设置  此队列中  消息的过期时间
        Map<String,Object> map=new HashMap<>();
        // 队列中 所有的消息的过期时间 为 20秒
        //map.put("x-message-ttl",20000);
        // 队列中 所有的消息的过期时间 为 5秒
        map.put("x-message-ttl",5000);
        //return new Queue(TTL_DIRECT_QUEUE,true,false,false);
        return new Queue(TTL_DIRECT_QUEUE,true,false,false,map);
    }

5.2 死信队列

死信队列:存放的是一些没有被及时消费的消息,也是一个普通的队列

死信交换机:Dead Letter Exchange,当消息成为死信后,会被重新发送到一个死信交换机,死信交换机也是一个普通的交换机

在以下三种情况会成为死信

  1. 队列的长度达到了极限
  2. 消费者拒收了消息(basic.reject/ basic.nack),并且没有即使入队(requeue = false)
  3. 消息在队列中过了存活时间且没有被消费

使用步骤

  1. 新建普通交换机,作为死信交换机
  2. 新建普通队列1,作为死信队列,绑定死信交换机
  3. 新建普通队列2,指定死信交换机,如果2中的消息成了死信,就会被转发到队列1中

5.3 延迟队列

实现方式:TTL+死信队列

实现步骤

  1. 生产者生产一条消息,根据需要延后处理的时间不同,使用路由模式放入到不同的队列中
  2. 创建死信交换机,将上面的队列绑定到死信交换机上,然后消息过期后,再通过路由key传入不同的死信队列
  3. 功能的实现只需要通过消费者监听对应的死信队列即可

使用场景

  1. 订单十分之内未支付则自动取消
  2. 新注册的账号,十天之内未操作则注销账号
  3. 用户发起退款,如果三天内没处理则自动通知运营人员

6. RabbitMQ的应用场景

6.1 异步处理

如果一个用户在注册成功后,需要给他发短信并发邮件进行提示;已经注册成功了,发短信和发邮件并不是最重要的,可以慢慢处理

  1. 串行的方式:需要这些步骤挨个完成,需要150ms
  2. 并行:注册50ms;然后开两个线程,等这两个线程执行完都返回ok需要50ms;共计100ms
  3. 使用消息队列:注册50ms;然后将信息放入队列;放入队列用时5ms;共计55ms

串行方式

06 RabbitMQ_第7张图片

并行方式

06 RabbitMQ_第8张图片

使用消息队列

06 RabbitMQ_第9张图片

6.2 应用解耦

这个很好理解;双十一的时候,订单系统直接调用库存系统;如果库存系统宕机了,整个系统就瘫痪了

加入消息队列后,两者靠消息队列进行连接;库存系统宕机后不会影响订单系统;消息可以暂时存放在消息队列中慢慢处理

在这里插入图片描述

改进后

06 RabbitMQ_第10张图片

6.3 流量削峰

秒杀活动的时候,由于抢的人太多可能会导致服务宕掉

可以使用消息队列,队列满了之后可以直接抛弃掉后面的请求

06 RabbitMQ_第11张图片

6.4 定时、延时任务

可以看上面的延时队列

补充:RabbitMQ结构图

简图

06 RabbitMQ_第12张图片

详细架构图

06 RabbitMQ_第13张图片

你可能感兴趣的:(面试题-2022年3月整理,java,开发语言,后端)