Bootstrap

Rabbit MQ

Rabbit MQ

1. Rabbit MQ介绍

  • 定义:

    • MQ:消息队列,用于系统与系统之间进行消息通信。
    • Rabbit MQ:是一款基于erlang语言,基于AMQP协议实现的MQ。
  • 作用:

    • 应用解耦:提升应用的可维护性和容错性,可扩展性
    • 异步并发:提升应用的并发吞吐性能,提高系统性能
    • 削峰填谷:提升应用的高可用
    • 分布式事务:提供分布式事务解决方案,解决数据的一致性、原子性等
  • 劣势:

    • 系统可用性降低:系统引入的外部依赖越多,系统稳定性越差。一旦MQ宕机,就会对业务造成影响,就不能保证MQ的高可用。
    • 系统复杂度提高:MQ的加入大大提高了系统的复杂度,以前系统之间是同步的远程调用,现在是通过MQ进行异步调用。则还需要保证消息没有被重复消费、消息丢失以及消息传递的顺序性。
    • 一致性问题:A处理完业务,通过MQ给B、C、D三个系统发消息数据,如果B、C都成功,D失败,如何保证消息一致性。
  • 场景:

    • 提供程序的扩展性
    • 提高程序性能
    • 高并发
  • 组成:

    • client
      • 生产者:producer
      • 消费者:consumer
    • server
      • 服务器:broker
        • Virtual Host:用于隔离不同业务的消息,类似于数据库
          • exchange:消息交换机
          • queue:消息存储队列
          • bingding:绑定exchange和queue之间的联系,routingKey(过滤消息的条件)
    • 关系:exchange和queue之间是多对多关系
      在这里插入图片描述
  • 其他消息中间件:
    在这里插入图片描述

2. docker安装Rabbit MQ

安装命令

3. spring 整合rabbitmq

3.1 生产者

  • 导依赖
 <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.1.7.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
            <version>2.1.8.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.1.7.RELEASE</version>
        </dependency>
    </dependencies>
  • 配置属性:登录信息(用户名、密码、虚拟机名、ip地址),放在properties配置文件
rabbitmq.host=192.168.5.128
rabbitmq.port=5672
rabbitmq.username=guest
rabbitmq.password=guest
  • 配置bean:加载properties配置文件,配置连接工厂、配置交换机、队列、交换机和队列的绑定
<!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbit.properties"/>
    <!-- 定义rabbitmq connectionFactory -->
    <rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}"
        port="${rabbitmq.port}"
        username="${rabbitmq.username}"
        password="${rabbitmq.password}"/>
    <!--定义管理交换机、队列-->
    <rabbit:admin connection-factory="connectionFactory"/>
    <!--定义持久化队列,不存在则自动创建;不绑定到交换机则绑定到默认交换机
  默认交换机类型为direct,名字为:"",路由键为队列的名称
  -->
    <!--
        id:bean的名称
        name:queue的名称
        auto-declare:自动创建
        auto-delete:自动删除。 最后一个消费者和该队列断开连接后,自动删除队列
        exclusive:是否独占
        durable:是否持久化
    -->

    <rabbit:queue id="spring_queue" name="spring_queue" auto-declare="true"/>
    <!--定义广播类型交换机;并绑定队列-->
    <rabbit:fanout-exchange id="spring_fanout_exchange" name="spring_fanout_exchange"  auto-declare="true">
        <rabbit:bindings>
            <rabbit:binding  queue="spring_queue"  />
        </rabbit:bindings>
    </rabbit:fanout-exchange>
    <!--定义rabbitTemplate对象操作可以在代码中方便发送消息-->
    <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>
  • 编码
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbit.xml")
public class ProducerTest {

    //1.注入 RabbitTemplate
    @Autowired
    private RabbitTemplate rabbitTemplate;


    @Test
    public void testHelloWorld(){
        //2.发送消息

        rabbitTemplate.convertAndSend("spring_queue","hello world spring....");
    }
 }

3.2 消费者

  • pom:同上
  • 属性配置:同上(密码等)
  • bean配置
<!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!-- 定义rabbitmq connectionFactory -->
    <rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}"
        port="${rabbitmq.port}"
        username="${rabbitmq.username}"
        password="${rabbitmq.password}"/>

    <bean id="springQueueListener" class="org.example.listener.SpringQueueListener"/>

    <rabbit:listener-container connection-factory="connectionFactory" auto-declare="true">
        <!--指定要监听的队列-->
        <rabbit:listener ref="springQueueListener" queue-names="spring_queue"/>
    </rabbit:listener-container>
  • 监听器

public class SpringQueueListener implements MessageListener {


    public void onMessage(Message message) {
        //打印消息
        System.out.println(new String(message.getBody()));
    }
}

  • 测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-consumer.xml")
public class ConsumerTest {

    @Test
    public void test1(){
        boolean flag = true;
        while (true){

        }
    }
}

3.3 测试

  1. 先启动producer,再启动consumer
  2. 当运行producer时,可以看到控制台会有消息
    在这里插入图片描述
  3. 此时还未启动consumer,消息并未被消费,应该先启动consumer进行测试。consumer的onMessage方法会监听到消息。

4. spring boot整合 Rabbit MQ

4.1 生产者

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
  • 属性配置
spring:
  rabbitmq:
    # ip
    host: 192.168.5.128 
    port: 5672
    username: guest
    password: guest
  • bean配置
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {

    public static final String EXCHANGE_NAME = "boot_topic_exchange";
    public static final String QUEUE_NAME = "boot_queue";

    /**
     * 定义一个交换机
     *
     * @return 交换机
     */
    @Bean("bootExchange")
    public Exchange bootExchange() {

        return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
    }

    /**
     * 定义一个队列
     *
     * @return 队列
     */
    @Bean("bootQueue")
    public Queue bootQueue() {
        return QueueBuilder.durable(QUEUE_NAME).build();
//        return QueueBuilder.durable(QUEUE_NAME).withArgument("x-message-ttl", "5000").build();
    }

    /**
     * 队列和交互机绑定关系 Binding 三要素:队列、交换机、routingKey
     */
    @Bean
    public Binding bindQueueExchange(@Qualifier("bootQueue") Queue queue,
                                     @Qualifier("bootExchange") Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs();
    }
}
  • 启动类:无其他注解
  • 测试类:
 @Autowired
    private RabbitTemplate template;

    @Test
    public void contextLoads() {
        // 发送消息
        template.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,// 指定交换机
                                "boot.haha",// 绑定关系
                                "boot mq hello~~~"// 信息
        );
    }

4.2 消费者

  • pom:同生产者
  • yml:同生产者
  • 监听器:
@Component
public class RabbimtMQListener {

    /**
     * 监听队列,消费消息
     *
     * @param message
     * @param channel
     */
    @RabbitListener(queues = "boot_queue")
    public void listenerQueue(Message message, Channel channel) {
        System.out.println(new String(message.getBody()));
    }

}

4.3 测试

先启动消费者的启动类。再运行生产者的测试类

4.4 异常

Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method<channel.close>(reply-code=406, reply-text=PRECONDITION_FAILED - invalid arg 'x-message-ttl' for queue 'boot_queue' in vhost '/': "expected integer, got longstr", class-id=50, method-id=10)
  • 异常环境:在创建队列时定义ttl
return QueueBuilder.durable(QUEUE_NAME).withArgument("x-message-ttl", (int)5000).build();
  • 原因:queue已经存在,但是启动时试图设定一个 x-dead-letter-exchange 参数,这和服务器上的定义不一样,server 不允许所以报错。如果删除 queue 重新 declare 则不会有问题。或者通过 policy 来设置这个参数也可以不用删除队列。

5. 如何保障消息不丢失

5.1 生产者的可靠性

如何保障消息有真正发送到Broker;

  • 事务的方式:性能低,一般不用
  • Confirm机制:异步双向确认消息的可靠
  • return:回退
5.1.1 confirm确认机制
  • yml
publisher-confirms: true
  • 设置接收broker确认消息的处理逻辑
    //2. 定义回调
            rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
                /**
                 *
                 * @param correlationData 相关配置信息
                 * @param ack   exchange交换机 是否成功收到了消息。true 成功,false代表失败
                 * @param cause 失败原因
                 */
                @Override
                public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                    System.out.println("confirm方法被执行了....");
    
                    if (ack) {
                        //接收成功
                        System.out.println("接收成功消息" + cause);
                    } else {
                        //接收失败
                        System.out.println("接收失败消息" + cause);
                        //做一些处理,让消息再次发送。
                    }
                }
            });
5.1.2 return回退机制

在这里插入图片描述

监控消息是否发送到queue

  • 开启return
publisher-returns: true
  • 设置无路由的消息不丢失

    • 无路由消息丢弃(默认)
    • 无路由消息返回【return选用的模式】
   //设置交换机处理失败消息的模式,true是返回模式,false是丢弃模式
   rabbitTemplate.setMandatory(true);
  • 监控无路由的消息
  //2.设置ReturnCallBack
          rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
              /**
               *
               * @param message   消息对象
               * @param replyCode 错误码
               * @param replyText 错误信息
               * @param exchange  交换机
               * @param routingKey 路由键
               */
              @Override
              public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                  System.out.println("return 执行了....");
  
                  System.out.println(message);
                  System.out.println(replyCode);
                  System.out.println(replyText);
                  System.out.println(exchange);
                  System.out.println(routingKey);
  
                  //处理
              }
          });

5.2 消费者的可靠性

在这里插入图片描述

如何保障消费者成功消费

  • ack:手动确认模式
  • 开启
listener:
      simple:
        acknowledge-mode: manual
  • 手动确认
@Component
public class AckListener implements ChannelAwareMessageListener {

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        try {
            //1.接收转换消息
            System.out.println(new String(message.getBody()));

            //2. 处理业务逻辑
            System.out.println("处理业务逻辑...");
            int i = 3/0;//出现错误
            //3. 手动签收,true是批量确认
            channel.basicAck(deliveryTag,true);
        } catch (Exception e) {
            //e.printStackTrace();

            //4.拒绝签收
            /*
            第三个参数:requeue:重回队列。如果设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
             */
            channel.basicNack(deliveryTag,true,true);
            //channel.basicReject(deliveryTag,true);
        }
    }
}

5.3 服务端的可靠性

  • 高可用、搭建集群;
  • 交换机、队列持久化;

6. 工作模式

6.1 简单模式 hello world

一个生产者一个消费者,使用默认的交换机。

6.2 工作队列模式 Work Queue

一个生产者,多个消费者(竞争关系),使用默认交换机。

  • Work Queues:与入门程序的简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。
  • 编码:多新增一个或多个消费者测试。
  • 多个消费者之间为竞争关系。
  • 应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度,例如:一个短信服务部署多个,只要有一个节点成功即可。
    在这里插入图片描述

6.3 发布订阅模式 Publish/subscribe

需要设置类型为fanout的交换机,并且交换机和队列绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列。在订阅模型中,多了一个 Exchange 角色,而且过程略有变化:

  • P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
  • C:消费者,消息的接收者,会一直等待消息到来
  • Queue:消息队列,接收消息、缓存消息
  • Exchange:交换机(X)。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、
    递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常见以下3种类型:
    • Fanout:广播,将消息交给所有绑定到交换机的队列
    • Direct:定向,把消息交给符合指定routing key 的队列
    • Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
  • Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与 Exchange 绑定,或者没有符合
    路由规则的队列,那么消息会丢失!
    在这里插入图片描述
  • 交换机需要与队列进行绑定,绑定之后;一个消息可以被多个消费者都收到。
  • 发布订阅模式与工作队列模式的区别:
    • 工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机
    • 发布/订阅模式的生产方是面向交换机发送消息,工作队列模式的生产方是面向队列发送消息(底层使用
      默认交换机)
    • 发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队
      列绑 定到默认的交换机

6.4 路由模式Routing

  • 需要设置类型为direct的交换机,交换机和队列进行绑定,并且制定routingkey,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列。
  • Routing 模式要求队列在绑定交换机时要指定 routing key,消息会转发到符合 routing key 的队列。
  • 队列与交换机的绑定,不能是任意绑定了,而是要指定一个 RoutingKey(路由key)
  • 消息的发送方在向 Exchange 发送消息时,也必须指定消息的 RoutingKey
  • Exchange 不再把消息交给每一个绑定的队列,而是根据消息的 Routing Key 进行判断,只有队列的Routingkey 与消息的 Routing key 完全一致,才会接收到消息
    在这里插入图片描述

6.5 通配符模式 Topic

  • Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过交换机是Topic 类型
  • Exchange 可以让队列在绑定 Routing key 的时候使用通配符
  • Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
  • 通配符规则:# 匹配一个或多个词,* 匹配不多不少恰好1个词,例如:item.# 能够匹配 item.insert.abc,或者 item.insert,item.* 只能匹配 item.insert。
    在这里插入图片描述

7. 限流机制

消费端每次从mq拉取一条消息来消费,直到手动确认消费完毕后,才会继续拉取下一条消息。

  • 属性配置
# springboot
spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1 # 必须在签收后才能收到下一条

8. ttl

  • 设置队列过期时间:x-message-ttl,单位:ms(毫秒),对整个队列消息统一过期。
  • 设置消息过期时间:expiration。单位:ms(毫秒),当消息在队列头部时(消费时),会单独判断这一消息是否过期。
  • 如果两者都进行了设置,以时间短的为准。

设置在队列上:

# springboot
QueueBuilder.durable(QUEUE_NAME)
            .withArgument("x-message-ttl",10000).build();

设置在消息上

MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                // 1. 设置message的信息
                message.getMessageProperties().setExpiration("5000");// 消息的过期时间
                // 返回该消息
                return message;
            }
        };
        for (int i=0;i<5;i++) {
            template.convertAndSend
                    (RabbitMQConfig.EXCHANGE_NAME, "boot.ha", "spring",messagePostProcessor);
        }

9. 死信队列

  • 被broker丢弃的消息即为死信
  • 什么时候丢弃:
    • TTL超时
    • 队列满,放不下的消息
    • 消费者标记丢弃的消息,消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
  • 死信队列:即存储被丢弃的队列,死信队列本质也是一个普通的队列
  • 队列参数:
    • x-dead-letter-exchange
    • x-dead-letter-routingKey
      在这里插入图片描述

9.1 搭建死信队列

  • 定义正常交换机正常队列。

  • 定义死信交换机,死信队列。

  • 正常队列绑定死信交换机(定义正常队列时就携带参数)

  • 配置类

@Configuration
public class RabbitMQConfig {

    public static final String EXCHANGE_NAME = "exchange_normal";
    public static final String DLX_EXCHANGE_NAME = "exchange_dlx";
    public static final String QUEUE_NAME = "queue_normal";
    public static final String DLX_QUEUE_NAME = "queue_dlx";

    /**
     * 定义正常交换机
     *
     * @return 交换机
     */
    @Bean(EXCHANGE_NAME)
    public Exchange bootExchange() {

        return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
    }

    /**
     * 定义正常队列
     *
     * @return 队列
     */
    @Bean(QUEUE_NAME)
    public Queue bootQueue() {
        return QueueBuilder.durable(QUEUE_NAME)
                           .withArgument("x-dead-letter-exchange", DLX_EXCHANGE_NAME)
                           .withArgument("x-dead-letter-routing-key", "dlx.hh")
                           .withArgument("x-message-ttl", 5000)
                           .withArgument("x-max-length", 5)
                           .build();
//        return QueueBuilder.durable(QUEUE_NAME).withArgument("x-message-ttl", (int)5000).build();
    }

    /**
     * 正常交换机绑定正常队列 队列和交互机绑定关系 Binding 三要素:队列、交换机、routingKey
     */
    @Bean
    public Binding bindQueueExchange(@Qualifier(QUEUE_NAME) Queue queue,
                                     @Qualifier(EXCHANGE_NAME) Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("normal.#").noargs();
    }


    /**
     * 定义死信交换机
     *
     * @return 交换机
     */
    @Bean(DLX_EXCHANGE_NAME)
    public Exchange exchangeDlx() {

        return ExchangeBuilder.topicExchange(DLX_EXCHANGE_NAME).durable(true).build();
    }

    /**
     * 定义死信队列
     *
     * @return 队列
     */
    @Bean(DLX_QUEUE_NAME)
    public Queue queueDlx() {
        return QueueBuilder.durable(DLX_QUEUE_NAME)
                           .build();
//        return QueueBuilder.durable(QUEUE_NAME).withArgument("x-message-ttl", (int)5000).build();
    }

    /**
     * 死信交换机绑定死信队列 队列和交互机绑定关系 Binding 三要素:队列、交换机、routingKey
     */
    @Bean
    public Binding bindQueueExchangeDlx(@Qualifier(DLX_QUEUE_NAME) Queue queue,
                                        @Qualifier(DLX_EXCHANGE_NAME) Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("dlx.#").noargs();
    }

  • 测试类
    @Test
    public void testConfirm() {
//        // 消息后处理对象,设置一些消息的参数信息
        MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                //1.设置message的信息
                message.getMessageProperties().setExpiration("3000");//消息的过期时间
                //2.返回该消息
                return message;
            }
        };
        //1. 测试过期时间,死信消息
        template.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "normal.haha",
                                      "message test....",messagePostProcessor);
        
        //2. 测试长度限制后,消息死信
//        for (int i = 0; i < 10; i++) {
//            template.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"normal.haha","我是一条消息,我会死吗?");
//        }
    }

在这里插入图片描述

9.2 参数

  • x-mesage-ttl:设置消息的过期时间
  • x-max-length:设置长度
  • x-dead-letter-exchange:设置死信交换机名称
  • x-dead-letter-routing-key:设置死信交换路由

10. 延迟队列

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。即死信+ttl

10.1 超时订单关闭

  • 需求:
    • 下单后,30分钟未支付,取消订单,回滚数据。
    • 新用户注册成功7天后,发送短信问候。
  • 实现:
    • 定时器
    • 延迟队列
      在这里插入图片描述

11. 消息幂等性

  • 多次操作与一次操作的结果相同(扣钱)
  • 分类:
    • 消息幂等性
    • 接口幂等性
  • 解决方案:
    • 数据库的乐观锁:(底层是互斥锁),设定数据版本,修改时带上版本号,防止同一版本的数据多次修改。
    • 数据库的日志表
      在这里插入图片描述
;