【分布式&微服务】RabbitMQ实践

一、MQ基础

1.什么是MQ

为什么会用到MQ呢?
答:1>在传统的系统中,当遇到相对复杂的业务中,一次用户请求可能会同步调用N个系统的接口,则需要等待所有的接口都返回了,才能真正的获取执行结果。这种同步调用接口的方式总耗时比较长,非常影响用户的体验,特别是在网络不稳定的情况下,非常容易出现接口超时问题。
【分布式&微服务】RabbitMQ实践_第1张图片
2>在进行系统设计的时候,一般会将复杂的业务系统拆分成为多个子系统。例如用户下单,请求会先通过订单系统,然后分别调用:支付系统、库存系统和物流系统等系统。系统之间耦合性太高,如果调用的任何一个子系统出现异常,整个请求都会异常,对系统的稳定性非常不利。
【分布式&微服务】RabbitMQ实践_第2张图片

3>秒杀活动,对系统的稳定性要求很高。如果用户比较少,则不会影响系统的稳定性。但如果用户突增,一时间所有的请求都到数据库,可能会导致数据库无法承受这么大的压力,响应变慢或者直接挂掉。
【分布式&微服务】RabbitMQ实践_第3张图片

那如何解决上述三个问题呢?
答:mq
解决问题一:同步接口调用导致响应时间长的问题,使用mq之后,将同步调用改成异步,能够显著减少系统响应时间。提升用户体验和系统吞吐量(单位时间内处理请求的数目)
【分布式&微服务】RabbitMQ实践_第4张图片
解决问题二:使用mq之后,我们只需要依赖于mq,避免了各个子系统间的强依赖问题。
订单系统作为消息生产者,只需要保证自己没问题就可以了,不会受到库存系统等业务子系统的异常影响,并且各个消费者业务子系统之间,也互不影响。这样就把之前复杂的业务子系统的依赖关系,转换为只依赖于mq的简单依赖,从而显著的降低了系统间的耦合度,提升容错性和可维护性。
【分布式&微服务】RabbitMQ实践_第5张图片

解决问题三:业务系统接收到用户请求之后,将请求直接发送到mq,然后消费者从mq中消费消息。如果出现请求峰值的情况,由于消费者的消费能力有限,通过限流操作按照自己的节奏来消费消息,多余的消息会保留在mq的队列中,不会对系统的稳定性造成影响。
【分布式&微服务】RabbitMQ实践_第6张图片
优势总结:
1>异步提速:提升用户体验和系统吞吐量
2>应用解耦:提高系统容错性和可维护性
3>削峰填谷:提高系统稳定性

劣势:
1>系统可用性降低:随着外部依赖引入的增多,系统稳定性将会越差。如果 MQ 宕机,就会对业务造成影响。(如何保证MQ的高可用?)
2>系统复杂度提高:MQ 的引入将会增加系统的复杂度,以前系统间是同步的远程调用,现在是通过 MQ 进行异步调用。(如何保证消息不被丢失等情况?)

2.什么是RabbitMQ

RabbitMQ是实现了高级消息队列协议(AMQP)的开源分布式消息中间件,服务端是用Erlang语言编写的。RabbitMQ 凭借其高可靠、易扩展、高可用及丰富的功能特性,不管是互联网行业还是传统行业都在大量地使用。
RabbitMQ 基础架构如下图:
【分布式&微服务】RabbitMQ实践_第7张图片
RabbitMQ 中的相关概念:
1>Broker:接收和分发消息的应用,RabbitMQ Server就是 Message Broker
2>Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost 创建 exchange/queue 等
3>Connection:publisher/consumer 和 broker 之间的 TCP 连接
4>Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的 channel 进行通讯,AMQP method 包含了channel id 帮助客户端和message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销
5>Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)
6>Queue:消息最终被送到这里等待 consumer 取走
7>Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据

RabbitMQ 提供了 6 种工作模式:简单模式、work queues、Publish/Subscribe 发布与订阅模式、Routing 路由模式、Topics 主题模式、RPC 远程调用模式(远程调用,使用的比较少)。
官网对应模式介绍:https://www.rabbitmq.com/getstarted.html
【分布式&微服务】RabbitMQ实践_第8张图片
工作模式总结:
1>简单模式:一个生产者、一个消费者,不需要设置交换机(使用默认的交换机);
2>工作队列模式 Work Queue: 一个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机)。
3>发布订阅模式 Publish/subscribe:需要设置类型为 fanout 的交换机,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列。
4>路由模式 Routing:需要设置类型为 direct 的交换机,交换机和队列进行绑定,并且指定 routing key,当发送消息到交换机后,交换机会根据 routing key 将消息发送到对应的队列。
5>通配符模式 Topic:需要设置类型为 topic 的交换机,交换机和队列进行绑定,并且指定通配符方式的 routing key,当发送消息到交换机后,交换机会根据 routing key 将消息发送到对应的队列。
Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。但是 Topic 类型的队列在绑定 Routing key 的时候使用通配符(通配符规则:# 匹配一个或多个词,* 匹配不多不少恰好1个词,例如:key.# 能够匹配 key.msg.a 或者 key.msg,key.* 只能匹配 key.msg)。

二、实践

0.基础配置

1> pom.xml

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger2</artifactId>
      <version>2.6.0</version>
    </dependency>
    <dependency>
      <groupId>io.springfox</groupId>
      <artifactId>springfox-swagger-ui</artifactId>
      <version>2.6.0</version>
    </dependency>

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

    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
      <version>1.9.6</version>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
    </dependency>
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-messaging</artifactId>
      </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.73</version>
    </dependency>
      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-test</artifactId>
          <scope>test</scope>
      </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-test</artifactId>
      <scope>test</scope>
    </dependency>

2>application.properties

server.port=8888
#基本配置
spring.rabbitmq.port=5672
spring.rabbitmq.username=root
spring.rabbitmq.password=root
spring.rabbitmq.addresses=localhost
spring.rabbitmq.virtual-host=/
#消息的可靠性投递
#1.消息发送方消息确认参数
#springboot版本为2.1.4的时候才有这个属性
#spring.rabbitmq.publisher-confirms=true
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.publisher-confirm-type=correlated
#2.消费者消息确认参数
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#并行消费者数量
spring.rabbitmq.listener.simple.concurrency=3
#最大并行消费者数量
spring.rabbitmq.listener.simple.max-concurrency=5
#消费端限流
spring.rabbitmq.listener.simple.prefetch=100

1.消息可靠性投递

1-1 应用场景

在使用RabbitMQ的时候,为了防止出现消息丢失或者投递失败场景,RabbitMQ提供了两种方式用来控制消息的投递可靠性模式:
1>confirm确认模式
2>return退回模式

rabbitmq 整个消息投递的路径为:producer—>exchange—>queue—>consumer
消息从 producer 到 exchange 则会返回一个 confirmCallback 。
消息从 exchange–>queue 投递失败则会返回一个 returnCallback 。

1-2 流程图

【分布式&微服务】RabbitMQ实践_第9张图片

1-3 代码实现

1-3-1 RabbitMq配置
#springboot版本为2.1.4的时候才有这个属性
#spring.rabbitmq.publisher-confirms=true
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.publisher-confirm-type=correlated
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class RabbitMqConfig {

    @Resource
    private RabbitTemplate rabbitTemplate;

    /**
     * 生产方的消息到达确认机制
     * ConfirmCallback 不管消息有没有正确到达exchange,都会被触发
     * --如果到达exchange,则confirm回调,ack=true
     * --如果未道道exchange,则confirm回调,ack=false
     * ReturnCallback 消息未正确到达队列时触发
     * --如果到达queue,则不会触发回调
     * --如果未到达queue,则触发回调
     *
     * @return
     */
    @PostConstruct
    public RabbitTemplate initRabbitTemplate() {

        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        **rabbitTemplate.setMandatory(Boolean.TRUE); // 是否允许执行returnCallback方法**

        // 不论消息是否正确到达交换机,都会触发回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             * @param correlationData 消息
             * @param ack 是否到达交换机的确认结果 true到达交换机 false未到达交换机
             * @param cause 未到达交换机的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                // CorrelationData springboot提供的参数,不是必须的。失败后返回的一个信息,一般就可以将失败的订单ID返回
                // 我们可以在发送消息的时候附带一个CorrelationData参数 这个对象可以设置一个id,可以是你的业务id 方便进行对应的操作
//                                CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
//                                rabbitTemplate.convertAndSend("directExchange", "direct.key123123", "hello",correlationData);
                //System.out.println("confirm方法被执行了...."+correlationData.getId());

                //ack 为  true表示 消息已经到达交换机
                if (ack) {
                    //接收成功
                    System.out.println("接收成功消息" + cause);
                } else {
                    //接收失败
                    System.out.println("接收失败消息" + cause);
                    //做一些处理,让消息再次发送。
                }

            }
        });

        **// 只有消息未正确到达队列时,才会回调**
        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:" + message);
                System.out.println("replyCode:" + replyCode);
                System.out.println("replyText:" + replyText);
                System.out.println("exchange:" + exchange);
                System.out.println("routingKey:" + routingKey);
            }
        });
        return rabbitTemplate;
    }

    //声明队列
    @Bean
    public Queue topicMqQ1() {
        return new Queue("topic_mq_q1");
    }

   //声明exchange
    @Bean
    public TopicExchange setTopicMqExchange() {
        return new TopicExchange("topicMqExchange");
    }

    //声明binding,需要声明一个routingKey
    @Bean
    public Binding bindTopicMqQ1() {
        return BindingBuilder.bind(topicMqQ1()).to(setTopicMqExchange()).with("topic_mq_q1_msg");
    }
}
1-3-2 消费者
    @RabbitListener(queues = "topic_mq_q1")
	public void topicReceiveMqq1Msg1(Channel channel, @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws Exception {
		try {
			// 业务代码
			System.out.println("Topic模式 topic_mq_q1 received  message1 : " +message);
		}  finally {
			channel.basicAck(deliveryTag,false);
		}
	}
1-3-3 测试类
    @Test
    public void test() {
        String str1 = "I am the queen of computers";
        rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str1);
    }

运行结果为:
在这里插入图片描述
【分布式&微服务】RabbitMQ实践_第10张图片

1-4 小结

消息的可靠性投递(消息发送方):
1> producer -> exchange
注:在springboot2.2.0.RELEASE 版本之前使用的是spring.rabbitmq.publisher-confirms来配置消息发送到交换器之后是否触发回调方法,但是在2.2.0及之后该属性不再使用,使用spring.rabbitmq.publisher-confirm-type属性配置代替。
1-1>设置ConnectionFactory的publisher-confirms=“true” 或 spring.rabbitmq.publisher-confirm-type=correlated 开启确认模式;
1-2>使用rabbitTemplate.setConfirmCallback设置回调函数。当消息发送到exchange后回调confirm方法。在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。

2> exchange -> queue
2-1>设置ConnectionFactory的publisher-returns=“true” 开启 退回模式;
2-2>设置rabbitTemplate.setMandatory(true);
2-3>使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到queue失败后,如果设置了rabbitTemplate.setMandatory(true)参数,则会将消息退回给producer。并执行回调函数returnedMessage。

2.Consumer ACK

2-1 应用场景

ack指Acknowledge,表示消费端收到消息后的确认方式。
有三种确认方式:
自动确认:acknowledge=“none”
手动确认:acknowledge=“manual”
根据异常情况确认:acknowledge=“auto”

其中自动确认是指,当消息一旦被Consumer接收到,则自动进行确认收到,并将相应消息从 RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能出现消息虽然接收到了但是业务处理出现了异常,但是此时该消息已经被自动确认了,就会出现消息丢失的情况。如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。

// 手动确认
channel.basicAck(deliveryTag,false);
// 当channel.basicNack 第三个参数设为true时,消息签收失败会继续进入消息队列等待消费
// 当channel.basicNack 第三个参数设为false时,消息签收失败,此时消息进入死信队列,完成消费
channel.basicNack(deliveryTag,false,false);

2-2 流程图

【分布式&微服务】RabbitMQ实践_第11张图片

2-3 代码实现

2-3-1 RabbitMq配置
spring.rabbitmq.listener.simple.acknowledge-mode=manual
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class RabbitMqConfig {

    //声明队列
    @Bean
    public Queue topicMqQ1() {
        return new Queue("topic_mq_q1");
    }

   //声明exchange
    @Bean
    public TopicExchange setTopicMqExchange() {
        return new TopicExchange("topicMqExchange");
    }

    //声明binding,需要声明一个routingKey
    @Bean
    public Binding bindTopicMqQ1() {
        return BindingBuilder.bind(topicMqQ1()).to(setTopicMqExchange()).with("topic_mq_q1_msg");
    }
}
2-3-2 消费者
@RabbitListener(queues="topic_mq_q1")
	public void topicReceiveMqq1Msg1(Channel channel, @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws Exception {
		System.out.println("Topic模式 topic_mq_q1 received  message : " +message);
		try {
			// 业务代码
		} finally {
			channel.basicAck(deliveryTag,false);
		}
	}
2-3-3 测试类
    @Test
    public void test() {
        String str1 = "I am the queen of computers";
        rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str1);
    }

运行结果为:
在这里插入图片描述

2-4 小结

消息的可靠性投递(消费端):
1>在rabbit:listener-container标签中设置acknowledge属性(spring.rabbitmq.listener.simple.acknowledge-mode),设置ack方式 none(自动确认)、manual(手动确认);
2>如果在消费端没有出现异常,则调用channel.basicAck(deliveryTag,false);方法确认签收消息;如果出现异常,则在catch中调用 basicNack或 basicReject,拒绝消息,让MQ重新发送消息。

3.消费端限流

3-1 应用场景

当RabbitMQ服务器积压了有上万条未处理的消息,我们随便打开一个消费者客户端,会出现这样情况: 巨量的消息瞬间全部推送过来,但是我们单个客户端无法同时处理这么多数据。当数据量特别大的时候,我们对消息发送端限流肯定是不科学的,因为有时候并发量就是特别大,有时候并发量又特别少,我们无法约束发送端,这是用户的行为。所以我们应该对消费端进行限流,用于保持消费端的稳定,当消息数量激增的时候很有可能造成资源耗尽,以及影响服务的性能,导致系统的卡顿甚至直接崩溃。

3-2 流程图

【分布式&微服务】RabbitMQ实践_第12张图片

3-3 代码实现

3-3-1 RabbitMq配置
spring.rabbitmq.listener.simple.concurrency=3
spring.rabbitmq.listener.simple.prefetch=100
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class RabbitMqConfig {

    //声明队列
    @Bean
    public Queue topicMqQ1() {
        return new Queue("topic_mq_q1");
    }

   //声明exchange
    @Bean
    public TopicExchange setTopicMqExchange() {
        return new TopicExchange("topicMqExchange");
    }

    //声明binding,需要声明一个routingKey
    @Bean
    public Binding bindTopicMqQ1() {
        return BindingBuilder.bind(topicMqQ1()).to(setTopicMqExchange()).with("topic_mq_q1_msg");
    }
}
3-3-2 消费者
@RabbitListener(queues="topic_mq_q1")
	public void topicReceiveMqq1Msg(Channel channel, @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
		System.out.println(deliveryTag + "-" +message);
		channel.basicAck(deliveryTag,false);
	}
3-3-3 测试类
@Test
    public  void testQos() throws InterruptedException {
       for (int i = 0; i < 10; i++) {
           rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg","test" + i);
       }
}

运行结果如下:
【分布式&微服务】RabbitMQ实践_第13张图片

@Test
    public  void testQos() throws InterruptedException {
       for (int i = 0; i < 500; i++) {
           rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg","test" + i);
       }
}

运行结果如下:
【分布式&微服务】RabbitMQ实践_第14张图片
【分布式&微服务】RabbitMQ实践_第15张图片

3-4 源码解析

项目中大部分使用@RabbitmqListener注解的方式处理业务代码中MQ的消费,这个注解用于监听指定的队列,如果containerFactory未指定,默认使用SimpleRabbitListenerContainerFactory实例对象创建一个消息监听容器(SimpleMessageListenerContainer)。默认情况下,rabbitmq的消费者为单线程串行消费,这也是队列的特性。从SimpleMessageListenerContainer的源码可以看到设置并发消费属性concurrentConsumers=1,从字面意义也可以分析出该字段是设置并发消费者的数量,默认为一个监听器设置一个消费者。

private volatile int concurrentConsumers = 1;

rabbitmq容器启动的时候根据设置的concurrentConsumers创建N个BlockingQueueConsumer(N个消费者队列)

protected int initializeConsumers() {
        int count = 0;
        synchronized(this.consumersMonitor) {
            if (this.consumers == null) {
                this.cancellationLock.reset();
                this.consumers = new HashSet(this.concurrentConsumers);

                for(int i = 0; i < this.concurrentConsumers; ++i) {
                    BlockingQueueConsumer consumer = this.createBlockingQueueConsumer();
                    this.consumers.add(consumer);
                    ++count;
                }
            }

            return count;
        }
    }

另外它继承的抽象类AbstractMessageListenerContainer的构造函数,代码中prefetchCount为设置并发消费的另一个关键属性,prefetchCount指一个消费者每次一次性从broker里面取出的待消费的消息个数,默认值prefetchCount=250。

public AbstractMessageListenerContainer() {
        this.proxy = this.delegate;
        this.shutdownTimeout = 5000L;
        this.transactionAttribute = new DefaultTransactionAttribute();
        this.taskExecutor = new SimpleAsyncTaskExecutor();
        this.recoveryBackOff = new FixedBackOff(5000L, 9223372036854775807L);
        this.messagePropertiesConverter = new DefaultMessagePropertiesConverter();
        this.missingQueuesFatal = true;
        this.possibleAuthenticationFailureFatal = true;
        this.autoDeclare = true;
        this.mismatchedQueuesFatal = false;
        this.failedDeclarationRetryInterval = 5000L;
        this.autoStartup = true;
        this.phase = 2147483647;
        this.active = false;
        this.running = false;
        this.lifecycleMonitor = new Object();
        this.queueNames = new CopyOnWriteArrayList();
        this.errorHandler = new ConditionalRejectingErrorHandler();
        this.exposeListenerChannel = true;
        this.acknowledgeMode = AcknowledgeMode.AUTO;
        this.deBatchingEnabled = true;
        this.adviceChain = new Advice[0];
        this.defaultRequeueRejected = true;
        **this.prefetchCount = 250;**
        this.lastReceive = System.currentTimeMillis();
        this.statefulRetryFatalWithNullMessageId = true;
        this.exclusiveConsumerExceptionLogger = new AbstractMessageListenerContainer.DefaultExclusiveConsumerLogger();
        this.lookupKeyQualifier = "";
        this.forceCloseChannel = true;
    }

上面我们已经根据concurrentConsumer的值设置了N个消费者队列,从下面代码中最后一行可以看出消费者队列中维护了一个阻塞队列,其中阻塞队列的大小就由prefetchCount决定。

public BlockingQueueConsumer(ConnectionFactory connectionFactory, MessagePropertiesConverter messagePropertiesConverter, ActiveObjectCounter<BlockingQueueConsumer> activeObjectCounter, AcknowledgeMode acknowledgeMode, boolean transactional, int prefetchCount, boolean defaultRequeueRejected, Map<String, Object> consumerArgs, boolean noLocal, boolean exclusive, String... queues) {
        this.cancelled = new AtomicBoolean(false);
        this.consumerArgs = new HashMap();
        this.deliveryTags = new LinkedHashSet();
        this.consumerTags = new ConcurrentHashMap();
        this.missingQueues = Collections.synchronizedSet(new HashSet());
        this.retryDeclarationInterval = 60000L;
        this.failedDeclarationRetryInterval = 5000L;
        this.declarationRetries = 3;
        this.connectionFactory = connectionFactory;
        this.messagePropertiesConverter = messagePropertiesConverter;
        this.activeObjectCounter = activeObjectCounter;
        this.acknowledgeMode = acknowledgeMode;
        this.transactional = transactional;
        this.prefetchCount = prefetchCount;
        this.defaultRequeueRejected = defaultRequeueRejected;
        if (consumerArgs != null && consumerArgs.size() > 0) {
            this.consumerArgs.putAll(consumerArgs);
        }

        this.noLocal = noLocal;
        this.exclusive = exclusive;
        this.queues = (String[])Arrays.copyOf(queues, queues.length);
        this.queue = new LinkedBlockingQueue(prefetchCount);
    }

根据队列的特性可知,如果阻塞队列中一个消息阻塞了,那么所有消息将会被阻塞,如果使用默认设置,concurrentConsumer=1,prefetchCount=250,那么当消费者队列中有一个消息由于某种原因阻塞了,那么该消息的后续消息同样不能被消费。为了防止这种情况的发生,我们可以增大concurrentConsumer的设置,使多个消费者可以并发消费。而prefetchCount该如何设置呢?假设conrrentConsumer=2,prefetchCount采用默认值,rabbitmq容器会初始化两个并发的消费者,每个消费者的阻塞队列大小为250,rabbitmq的机制是将消息投递给consumer1,先为consumer1投递满250个message,再往consumer2投递250个message,如果consumer1的message一直小于250个,consumer2一直处于空闲状态,那么并发消费退化为单消费者。
关于concurrentConsumer的设置有两种方式,一种是单个固定的值,如concurrentConsumer=4,另一种是concurrentConsumer=1-4。

public void setConcurrency(String concurrency) {
        try {
            int separatorIndex = concurrency.indexOf(45);
            if (separatorIndex != -1) {
                this.setConcurrentConsumers(Integer.parseInt(concurrency.substring(0, separatorIndex)));
                this.setMaxConcurrentConsumers(Integer.parseInt(concurrency.substring(separatorIndex + 1, concurrency.length())));
            } else {
                this.setConcurrentConsumers(Integer.parseInt(concurrency));
            }

        } catch (NumberFormatException var3) {
            throw new IllegalArgumentException("Invalid concurrency value [" + concurrency + "]: only single fixed integer (e.g. \"5\") and minimum-maximum combo (e.g. \"3-5\") supported.");
        }
    }

concurrency即为我们设置的参数,45为’-’的ascii码,容器首先设置了一个并发消费者,然后设置了最大并发消费者。maxConcurrentConsumer用于处理在极端情况下,可以实例化的最大的消费者数量。可以对比理解成线程池的核心线程数与最大线程数,在每次消费之初都会判断maxConcurrentConsumers是否为空,如果非空会判断是否对消费者进行弹性扩容,其中consecutiveMessages与consecutiveIdles变量控制是需要新增/减少消费者的标志位,对应的参考值分别为consecutiveActiveTrigger和consecutiveIdleTrigger,两个变量的默认值为10。

if (consecutiveMessages++ > SimpleMessageListenerContainer.this.consecutiveActiveTrigger) {
	SimpleMessageListenerContainer.this.considerAddingAConsumer();
    consecutiveMessages = 0;
}

if (consecutiveIdles++ > SimpleMessageListenerContainer.this.consecutiveIdleTrigger) {
    SimpleMessageListenerContainer.this.considerStoppingAConsumer(this.consumer);
    consecutiveIdles = 0;
}

当单个消费者连续接受的消息数量达到10个的时候,开始调用considerAddingAConsumer,判断时间是否满足要求对并发消费者进行扩容,反之就是减少消费者数量。

private void considerAddingAConsumer() {
        synchronized(this.consumersMonitor) {
            if (this.consumers != null && this.maxConcurrentConsumers != null && this.consumers.size() < this.maxConcurrentConsumers) {
                long now = System.currentTimeMillis();
                if (this.lastConsumerStarted + this.startConsumerMinInterval < now) {
                    this.addAndStartConsumers(1);
                    this.lastConsumerStarted = now;
                }
            }

        }
    }

3-5 小结

RabbitMQ并发消费的两个参数prefetchCount(spring.rabbitmq.listener.simple.prefetch)和concurrentConsumers(spring.rabbitmq.listener.simple.concurrency):
concurrentConsumers是设置并发消费者的个数,可以进行初始化-最大值动态调整,并发消费者可以提高消息的消费能力,防止消息的堆积。
prefechCount是每个消费者一次性从broker中取出的消息个数,提高这个参数并不能对消息实现并发消费,仅仅是减少了网络传输的时间。
注:1>在rabbit:listener-container 中配置 prefetch属性设置消费端一次拉取多少消息
2>消费端的确认模式一定为手动确认。acknowledge=“manual”

4.TTL

4-1 应用场景

TTL(Time To Live,生存时间),当消息到达存活时间后,还没有被消费,会被自动清除。
RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。

4-2 流程图

4-3 代码实现

4-3-1 RabbitMq配置
 @Bean("topicMqTtlQ1")
    public Queue topicMqTtlQ1() {
        return QueueBuilder.durable("topic_mq_ttl_q1").ttl(10000).build();
    }

    @Bean("topicMqTtlExchange")
    public TopicExchange setTopicMqTtlExchange() {
        return new TopicExchange("topicMqTtlExchange");
    }

    @Bean
    public Binding bindTopicMqTtlQ1(@Qualifier("topicMqTtlExchange") TopicExchange topicExchange,@Qualifier("topicMqTtlQ1") Queue queue) {
        return BindingBuilder.bind(queue).to(topicExchange).with("topic_mq_ttl_q1_msg");
    }
4-3-2 消费者

无,10s后队列中的数据会自动消失

4-3-3 测试类
@Test
    public void testTtl() {
        rabbitTemplate.convertAndSend("topicMqTtlExchange","topic_mq_ttl_q1_msg","test");
    }

4-4 小结

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

 @Test
    public void testMsgTtl() throws UnsupportedEncodingException {
        String message = "I am the queen of computers";
        //设置部分请求参数
        MessageProperties messageProperties = new MessageProperties();
        messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN);
        messageProperties.setExpiration("1000");
        //发消息
        rabbitTemplate.send("topicMqExchange","topic_mq_q1_msg",new Message(message.getBytes(StandardCharsets.UTF_8),messageProperties));
    }

5.延迟队列

5-1 应用场景

1>下单后,30分钟未支付,取消订单,回滚库存;订单成功支付则什么都不做。
2>新用户注册成功7天后,发送短信问候。

5-2 流程图

【分布式&微服务】RabbitMQ实践_第16张图片

5-3 代码实现

5-3-1 RabbitMq配置

1>application.properties

server.port=8888
spring.rabbitmq.port=5672
spring.rabbitmq.username=root
spring.rabbitmq.password=root
spring.rabbitmq.addresses=localhost
spring.rabbitmq.virtual-host=/
spring.rabbitmq.publisher-confirm-type=CORRELATED
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.listener.simple.acknowledge-mode=manual

2>RabbitMqConfig

package com.practice.springboot.config;

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;


@Configuration
public class RabbitMqConfig {

	@Resource
	private RabbitTemplate rabbitTemplate;

	
	@Bean
	public Queue topicMqQ2() {
		Map<String, Object> arguments = new HashMap<>();
		arguments.put("x-dead-letter-exchange","topicMqDelayExchange");
		arguments.put("x-dead-letter-routing-key","topic_mq_delay_q2_msg");
		arguments.put("x-message-ttl",10000);
		return new Queue("topic_mq_q2",true,false,false,arguments);
	}
	
	@Bean
	public Queue topicMqDelayQ2() {
		return new Queue("topic_mq_delay_q2");
	}

	//声明exchange
	@Bean
	public TopicExchange setTopicMqExchange() {
		return new TopicExchange("topicMqExchange");
	}
	
	@Bean
	public TopicExchange setTopicMqDelayExchange() {
		return new TopicExchange("topicMqDelayExchange");
	}

	//声明binding,需要声明一个routingKey
	@Bean
	public Binding bindTopicMqQ2() {
		return BindingBuilder.bind(topicMqQ2()).to(setTopicMqExchange()).with("topic_mq_q2_msg");
	}
	@Bean
	public Binding bindTopicMqDelayQ2() {
		return BindingBuilder.bind(topicMqDelayQ2()).to(setTopicMqDelayExchange()).with("topic_mq_delay_q2_msg");
	}


}
5-3-2 消费者
 @RabbitListener(queues="topic_mq_delay_q2")
    public void topicReceiveMqDelayQ2Msg(Channel channel, @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
		try {
			// 业务代码
			System.out.println("Topic模式 topic_mq_delay_q2 received  message : " +message);
		}  finally {
			channel.basicAck(deliveryTag,false);
		}
    }
5-3-3 测试类
package com.practice;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;

@SpringBootTest
@RunWith(SpringRunner.class)
public class ProducerTest {


    @Resource
    private RabbitTemplate rabbitTemplate;

    /*
     * 测试延时消息
     * */
    @Test
    public  void testDelay() throws InterruptedException {
        //1.发送消息
        rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q2_msg","test");


        //2.打印倒计时10秒
        for (int i = 10; i > 0 ; i--) {
            System.out.println(i+"...");
            Thread.sleep(1000);
        }

    }

}

运行结果如下:
【分布式&微服务】RabbitMQ实践_第17张图片

5-4 源码解析

5-5 小结

1> 延迟队列 指消息进入队列后,可以被延迟一定时间,再进行消费。
2>RabbitMq虽没有提供延迟队列功能,但是可以使用 : TTL + DLX 来达到延迟队列效果。

6.死信队列

6-1 应用场景

死信,在官网中对应的单词为“Dead Letter”,“死信”是RabbitMQ中的一种消息机制,当你在消费消息时,如果队列里的消息出现以下情况:
1>消息被否定确认,使用 channel.basicNack 或 channel.basicReject ,并且此时requeue 属性被设置为false。
2>消息在队列的存活时间超过设置的生存时间(TTL)时间。
3>消息队列的消息数量已经超过最大队列长度。
那么该消息将成为“死信”。
“死信”消息会被RabbitMQ进行特殊处理,如果配置了死信队列信息,那么该消息将会被丢进死信队列中,如果没有配置,则该消息将会被丢弃。

死信队列的配置:
1>配置业务队列,绑定到业务交换机上
2>为业务队列配置死信交换机和路由key(x-dead-letter-exchange 和 x-dead-letter-routing-key)
3>为死信交换机配置死信队列

6-2 流程图

【分布式&微服务】RabbitMQ实践_第18张图片

6-3 代码实现

基础队列消费失败后,进行重试队列,重试三次后进入死信队列

6-3-1 RabbitMq配置
package com.practice.springboot.config;

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;


/*
Topics模式  交换机类型 topic
* */
@Configuration
public class RabbitMqConfig {

	@Resource
	private RabbitTemplate rabbitTemplate;

	//声明队列
	@Bean
	public Queue topicMqQ1() {
		return new Queue("topic_mq_q1");
	}
	@Bean
	public Queue topicMqRetryQ1() {
		// x-dead-letter-exchange指定重试时将消息重发给哪一个转发器、x-message-ttl消息到达重试队列后,多长时间后重发
		Map<String, Object> arguments = new HashMap<>();
		arguments.put("x-dead-letter-exchange","topicMqDelayExchange");
		arguments.put("x-dead-letter-routing-key","topic_mq_delay_q1_msg");
		arguments.put("x-message-ttl",10000);
		return new Queue("topic_mq_retry_q1",true,false,false,arguments);
		//return QueueBuilder.durable("topic_mq_retry_q1").deadLetterExchange("directDelayExchange").deadLetterRoutingKey("delay.china.changsha").ttl(10000).build();
	}
	@Bean
	public Queue topicMqDelayQ1() {
		return new Queue("topic_mq_delay_q1");
	}
	//声明exchange
	@Bean
	public TopicExchange setTopicMqExchange() {
		return new TopicExchange("topicMqExchange");
	}
	@Bean
	public TopicExchange setTopicMqRetryExchange() {
		return new TopicExchange("topicMqRetryExchange");
	}
	@Bean
	public TopicExchange setTopicMqDelayExchange() {
		return new TopicExchange("topicMqDelayExchange");
	}

	//声明binding,需要声明一个routingKey
	@Bean
	public Binding bindTopicMqQ1() {
		return BindingBuilder.bind(topicMqQ1()).to(setTopicMqExchange()).with("topic_mq_q1_msg");
	}
	@Bean
	public Binding bindTopicMqRetryQ1() {
		return BindingBuilder.bind(topicMqRetryQ1()).to(setTopicMqRetryExchange()).with("topic_mq_retry_q1_msg");
	}
	@Bean
	public Binding bindTopicMqDelayQ1() {
		return BindingBuilder.bind(topicMqDelayQ1()).to(setTopicMqDelayExchange()).with("topic_mq_delay_q1_msg");
	}
}
6-3-2 消费者
	@RabbitListener(queues="topic_mq_q1")
	public void topicReceiveMqq1Msg1(Channel channel, @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws Exception {
		// MessagingMessageListenerAdapter.invokeHandlerAndProcessResult
		// Message message ->  @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag
		//  @Headers Map messageMap
		System.out.println("Topic模式 topic_mq_q1 received  message : " +message);
		try {
			// 业务代码
			businessHandle();
		} catch (Exception e) {
			MessageVo vo = new MessageVo(message,1);
			retryHandle(vo,"topicMqRetryExchange","topic_mq_retry_q1_msg");
		} finally {
			channel.basicAck(deliveryTag,false);
		}
	}

	@RabbitListener(queues="topic_mq_retry_q1")
	public void topicReceiveMqRetryQ1Msg(Channel channel, @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws Exception {
		MessageVo vo = JSON.parseObject(message, MessageVo.class);
		if (vo.getRetryCount() > 3) {
			// 超过重试次数则不重回队列
			// 当channel.basicNack 第三个参数设为true时,消息签收失败会继续进入消息队列等待消费
			// 当channel.basicNack 第三个参数设为false时,消息签收失败,此时消息进入死信队列,完成消费
			channel.basicNack(deliveryTag,false,false);
			System.out.println("超出重试上限,手动进入死信队列");
			return;
		}
		System.out.println("Topic模式 topic_mq_retry_q1 received  message : " +message + ",重试第"+ vo.getRetryCount() + "次");
		try {
			// 执行业务代码
			businessHandle();
		} catch (Exception e) {
			vo.setRetryCount(vo.getRetryCount()+1);
			retryHandle(vo,"topicMqRetryExchange","topic_mq_retry_q1_msg");
		} finally {
			channel.basicAck(deliveryTag,false);
		}
	}

	@RabbitListener(queues="topic_mq_delay_q1")
	public void topicReceiveMqDelayQ1Msg(Channel channel, @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
		System.out.println("Topic模式 topic_mq_delay_q1 received  message : " +message);
		channel.basicAck(deliveryTag,false);
	}

	private void businessHandle() throws Exception{
		// todo
		int i = 1/0;
	}

	private void retryHandle(MessageVo vo,String retryExchange,String routingKey) throws Exception{
		MessageProperties messageProperties = new MessageProperties();
		messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN);
		//messageProperties.setExpiration("10000"); // 设置过期时间
		String message = JSONObject.toJSONString(vo);
		rabbitTemplate.send(retryExchange,routingKey,new Message(message.getBytes(StandardCharsets.UTF_8),messageProperties));
	}
6-3-3 测试类
  @Test
    public void test() {
        String str1 = "I am the queen of computers";
        rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str1);
    }

6-4 源码解析

6-5 小结

7.消息幂等性保障

场景1:消费者在消费完一条消息后,向RabbitMQ 发送一个ACK 确认,但是此时网络断开或者其他原因导致RabbitMQ 没有收到这个ACK,那么RabbitMQ 并不会讲该条消息删除,而是重回队列,当客户端重新建立到连接后,消费者还是会再次收到该条消息,这就造成了消息的重复消费。
场景2:消息在发送的时候,同一条消息也可能发送多次。

解决方案
1>生成全局id,存入redis或者数据库,在消费者消费消息之前,查询一下该消息是否有消费过。
2>如果该消息已经消费过,则告诉mq消息已经消费,将该消息丢弃(手动ack)。
3>如果没有消费过,将该消息进行消费并将消费记录写进redis或者数据库中。
注:还有一种方式,数据库操作可以设置唯一键(消息id),防止重复数据的插入,这样插入只会报错而不会插入重复数据。

8.消息积压

1>消费者宕机积压
2>消费者消费能力不足积压
3>发送者发流量太大

解决方案:上线更多的消费者,进行正常消费上线专门的队列消费服务,将消息先批量取出来,记录数据库,再慢慢处理

三、问题

1.@RabbitListener和@RabbitHandler必须一起使用吗?

答:不是
@RabbitListener可以用于方法和类上,
1>@RabbitListener标注在方法上,直接监听指定的队列,此时接收的参数需要与发送类型一致
发布端:

 @Test
    public void testRabbitHandler() {
        String str1 = "I am the queen of computers";
        rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str1);
    }

消费端:

@RabbitListener(queues = "topic_mq_q1")
	public void topicReceiveMqq1Msg1(Channel channel, @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws Exception {
		try {
			// 业务代码
			System.out.println("Topic模式 topic_mq_q1 received  message1 : " +message);
		}  finally {
			channel.basicAck(deliveryTag,false);
		}
	}

运行结果为:
在这里插入图片描述
但是,同时有其它类型的参数会出现报错吗?答案是不会,默认将类型转换成了字符串。

 @Test
    public void testRabbitHandler() {
        String[] str = new String[]{"I"," am ","the queen of computers......"};
        rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str);
        String str1 = "I am the queen of computers";
        rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str1);
        char[] str2 = new char[]{'q','u','e','e','n'};
        rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str2);
}

运行结果为:
在这里插入图片描述
【分布式&微服务】RabbitMQ实践_第19张图片
2>@RabbitListener 可以标注在类上面,需配合 @RabbitHandler 注解一起使用
@RabbitListener 标注在类上面表示当有收到消息的时候,就交给 @RabbitHandler 的方法处理,根据接受的参数类型进入具体的方法中。
发布端:

 @Test
    public void testRabbitHandler() {
        String[] str = new String[]{"I"," am ","the queen of computers......"};
        rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str);
        String str1 = "I am the queen of computers";
        rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str1);
        char[] str2 = new char[]{'q','u','e','e','n'};
        rabbitTemplate.convertAndSend("topicMqExchange","topic_mq_q1_msg",str2);
    }

消费端:

@Component
@RabbitListener(queues = "topic_mq_q1")
public class TopicConcumerReceiver {

	@Autowired
	private RabbitTemplate rabbitTemplate;

	@RabbitHandler
	public void topicReceiveMqq1Msg1(Channel channel, @Payload String message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws Exception {
		try {
			// 业务代码
			System.out.println("Topic模式 topic_mq_q1 received  message1 : " +message);
		}  finally {
			channel.basicAck(deliveryTag,false);
		}
	}

	@RabbitHandler
	public void topicReceiveMqq1Msg1(Channel channel, @Payload String[] message, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws Exception {
		try {
			// 业务代码
			System.out.println("Topic模式 topic_mq_q1 received  message2 : " + Arrays.toString(message));
		}  finally {
			channel.basicAck(deliveryTag,false);
		}
	}	
}

运行结果为:
在这里插入图片描述
队列中有一条消息并未消费掉,是因为没有对应的char[]类型的消息体进行消费。在这里插入图片描述
重新运行单元测试的方法,会存在报错信息:该报错指的是消费队列中的那一条消息的时候找不到对应的消费者,此时队列中会出现两条未被消费的消息。

2.消息消费的顺序性

为什么要顺序消费?
保证消息的顺序消费是生产业务场景下经常面临的挑战,例如电商的下单逻辑,在用户下单之后,会发送创建订单和扣减库存的消息,我们需要保证扣减库存在创建订单之后执行。
1>处理业务逻辑后,向MQ发送一条消息,再由消费者从 MQ 中获取 消息落盘到MySQL 中。
2>在这个过程中,可能会有增删改的操作,比如执行顺序是增加、修改、删除。
3>消费者可能换了顺序给执行成删除、修改、增加,所以我们要保证消息的顺序消费。

为什么会不按顺序消费?
对于 RabbitMQ 来说,导致上面顺序错乱的原因通常是消费者是集群部署,不同的消费者消费到了同一订单的不同的消息。
1>如消费者1执行了增加,消费者2执行了修改,消费者C执行了删除
2>但是消费者C执行比消费者B快,消费者B又比消费者A快,就会导致消费消息的时候顺序错乱
3>本该顺序是增加、修改、删除,变成了删除、修改、增加.
【分布式&微服务】RabbitMQ实践_第20张图片

如何解决?
RabbitMQ 的问题是由于不同的消息都发送到了同一个 queue 中,多个消费者都消费同一个 queue 的消息。

1>我们可以给 RabbitMQ 创建多个 queue,每个消费者固定消费一个 queue 的消息,
1>生产者发送消息的时候,同一个类型的消息发送到同一个 queue 中
1>由于同一个 queue 的消息是一定会保证有序的,那么同一个订单号的消息就只会被一个消费者顺序消费,从而保证了消息的顺序性。
【分布式&微服务】RabbitMQ实践_第21张图片

3.消息重复问题

出现场景:
1>消息生产者产生了重复的消息
2>kafka和rocketmq的offset被回调了
3>消息消费者确认失败
4>消息消费者确认时超时了
5>业务系统主动发起重试

不管是由于生产者产生的消息重复,还是由于消费者导致的消息重复,我们都可以在消费者中来进行解决。(消费者在做业务处理时,要做幂等设计

推荐方法:增加一张消费消息表,使用messageId做唯一索引,在处理业务逻辑之前,先根据messageId查询一下该消息有没有处理过,如果已经处理过了则直接返回成功,如果没有处理过,则继续做业务处理。

4.数据一致性问题

数据一致性分为:强一致性、弱一致性和最终一致性
MQ中遵循的是最终一致性,因此在消费者消费失败后才会增加了重试机制。
重试分为同步重试和异步重试,
同步重试:有些消息量比较小的业务场景,可以采用同步重试,在消费消息时如果处理失败,立刻重试3-5次,如果还是失败,则写入到记录表中。但如果消息量比较大,则不建议使用这种方式,因为如果出现网络异常,可能会导致大量的消息不断重试,影响消息读取速度,造成消息堆积。
异步重试:而消息量比较大的业务场景,建议采用异步重试,在消费者处理失败之后,立刻写入重试表,添加一个job专门定时去重试。还有一种做法是,如果消费失败,自己给同一个topic发一条消息,在后面的某个时间点,自己又会消费到那条消息,起到了重试的效果。但是仅适用于对消息顺序要求不高的场景。

5.消息丢失问题

为了解决这个问题,可以增加一张消息发送表,当生产者发完消息之后,会往该表中写入一条数据,状态status标记为待确认。如果消费者读取消息之后,则更新该消息的status为已确认。

你可能感兴趣的:(分布式&微服务,rabbitmq,分布式,微服务)