RabbitMQ

MQ基础

同步调用存在的问题

  • 耦合度高:每次加入新的需求,都要修改原来的代码
  • 性能下降:调用者需要等待服务提供者响应,如果嗲用链过长则相应时间等于每次调用的之间之和
  • 资源浪费:调用链中的每个服务在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源
  • 级联失败:如果服务提供者出现问题,所有调用方都会跟着出问题

异步调用常见实现就是事件驱动模式

当支付服务发布支付成功事件后会发送一个订单id给Broker

Broker通知服务端进行工作

通知成功后Broker通知支付服务通知成功(不需要服务端完成工作)

异步通信的优点:

  • 耦合度低
  • 吞吐量提升
  • 故障隔离
  • 流量削枫

异步通信的缺点

  • 依赖于Broker的可靠性、安全性、吞吐能力
  • 架构复杂,业务没有明显的刘成希那,不好追踪管理

MQ(消息队列):存放消息的队列

MQ的五种消息模型

基本消息队列

发布消息者

java 复制代码
package cn.itcast.mq.helloworld;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.junit.Test;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class PublisherTest {
    @Test
    public void testSendMessage() throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("192.168.150.101");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("itcast");
        factory.setPassword("123321");
        // 1.2.建立连接
        Connection connection = factory.newConnection();

        // 2.创建通道Channel
        Channel channel = connection.createChannel();

        // 3.创建队列
        String queueName = "simple.queue";
        channel.queueDeclare(queueName, false, false, false, null);

        // 4.发送消息
        String message = "hello, rabbitmq!";
        channel.basicPublish("", queueName, null, message.getBytes());
        System.out.println("发送消息成功:【" + message + "】");

        // 5.关闭通道和连接
        channel.close();
        connection.close();

    }
}

消费者

java 复制代码
package cn.itcast.mq.helloworld;

import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class ConsumerTest {

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("192.168.150.101");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("itcast");
        factory.setPassword("123321");
        // 1.2.建立连接
        Connection connection = factory.newConnection();

        // 2.创建通道Channel
        Channel channel = connection.createChannel();

        // 3.创建队列
        String queueName = "simple.queue";
        channel.queueDeclare(queueName, false, false, false, null);

        // 4.订阅消息
        channel.basicConsume(queueName, true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
                // 5.处理消息
                String message = new String(body);
                System.out.println("接收到消息:【" + message + "】");
            }
        });
        System.out.println("等待接收消息。。。。");
    }
}

SpringAMQP

AMQP是用于在应用程序或之间传递业务消息的开放标准。该协议与语言和平台无关,丰富和微服务中独立性的要求

SpringAMQP:基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中spring-amqp是基础抽象,spring-rabbit是底层的默认实现

坐标

xml 复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

配置添加mq连接信息

java 复制代码
spring:
  rabbitmq:
    host: 192.168.150.101 # 主机名
    port: 5672 # 端口
    virtual-host: / # 虚拟主机
    username: itcast # 用户名
    password: 123321 # 密码

发送消息

java 复制代码
package cn.itcast.mq.spring;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void test(){
        String queueName = "simple.queue";          // 队列名称
        String message = "hello";                   // 消息
        rabbitTemplate.convertAndSend(queueName,message);
    }
}

消费者编写监听器监听队列消息

java 复制代码
package cn.itcast.mq;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class SpringRabbitListener {
    
    @RabbitListener(queues = "simple.queue")
    public void listenSimpleQueue(String msg){
        System.out.println(msg);
    }
    
}

WorkQueue模型


消息预取机制:消费者会提前将消息取走,等待后期慢慢处理

配置预取数量限制

java 复制代码
spring:
  rabbitmq:
    host: 192.168.150.101 # 主机名
    port: 5672 # 端口
    virtual-host: / # 虚拟主机
    username: itcast # 用户名
    password: 123321 # 密码
    listener:      
      simple:        
        prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息

发布订阅模式

这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。

实现思路如下:

  1. 在consumer服务中,利用代码声明队列、交换机,并将两者绑定
  2. 在consumer服务中,编写两个消费者方法,分别监听fanout.queue1和fanout.queue2
  3. 在publisher中编写测试方法,向itcast.fanout发送消息

    声明队列、交换机,并将两者绑定
java 复制代码
package cn.itcast.mq;

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

@Configuration
public class FanoutConfig {

    // 声明FanoutExchange交换机
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("itcast.fanout");
    }


    // 声明第1个队列
    @Bean
    public Queue fanoutQueue1(){
        return new Queue("fanout.queue1");
    }

    //绑 定队列1和交换机
    @Bean
    public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
    }

    @Bean
    public Queue fanoutQueue2(){
        return new Queue("fanout.queue2");
    }

    //绑 定队列2和交换机
    @Bean
    public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
    }

}

消费者代码不变

发送消息

java 复制代码
@Test
    public void test2(){
        // 交换机名称
        String exchangeName = "itcast.fanout";      // 指定交换机
        String message = "hello";                   // 消息
        // 将消息发送给所有消费者
        rabbitTemplate.convertAndSend(exchangeName,"",message);
    }

DirectExchange(路由)

DirectExchange会将接收到的消息根据规则路由到指定的队列中,称为路由模式

  1. 每个队列都会与Exchange设置一个BindingKey
  2. 发布消息时指定RoutingKey
  3. Exchange将消息路由到BindingKey与消息RoutingKey一致的队列

实现思路如下:

  1. 利用@RabbitListener声明Exchange、Queue、RoutingKey
  2. 在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2
  3. 在publisher中编写测试方法,向itcast.direct发送消息

绑定交换机

java 复制代码
package cn.itcast.mq;

import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FanoutConfig {

    // 声明FanoutExchange交换机
    @Bean
    public FanoutExchange fanoutExchange(){
        return new FanoutExchange("itcast.fanout");
    }


    // 声明第1个队列
    @Bean
    public Queue fanoutQueue1(){
        return new Queue("fanout.queue1");
    }

    //绑 定队列1和交换机
    @Bean
    public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
    }

    @Bean
    public Queue fanoutQueue2(){
        return new Queue("fanout.queue2");
    }

    //绑 定队列2和交换机
    @Bean
    public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
    }


    @RabbitListener(bindings = @QueueBinding(
            value = @org.springframework.amqp.rabbit.annotation.Queue,         // 队列名称
            exchange = @Exchange(name = "itcast.direct",type = ExchangeTypes.DIRECT),  // 交换机
            key = {"red", "yellow"}                     //key
            ))
    public void listenDirectQueue2(String msg){    
        System.out.println("消费者2接收到Direct消息:【"+msg+"】 ");
    }
}

发送消息

java 复制代码
    @Test
    public void test2(){
        // 交换机名称
        String exchangeName = "itcast.fanout";      // 指定交换机
        String message = "hello";                   // 消息
        rabbitTemplate.convertAndSend(exchangeName,"blue",message);         //blue 是routingkey
    }

接收消息不变

TopicExchange

TopicExchange和DirectExchange类似,但是TopicExchange的routingkey必须由多个单词的列表组成,中间用"."隔开

Queue与Exchange指定Bindingkey可以使用通配符

#:代表0各或多个单词

*:代表一个单词

消息转换

Spring的对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的。而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化。

如果要修改只需要定义一个MessageConverter 类型的Bean即可。推荐用JSON方式序列化,步骤如下:

导入坐标(生产者消费者都要引入)

xml 复制代码
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>

配置类声明MessageConverter:(服务端和消费端都要配置)

java 复制代码
@Bean
    public MessageConverter jsonMessageConverter(){    
        return new Jackson2JsonMessageConverter(); 
    }

生产者可靠性

生产者重连:客户端可能会连接MQ失败,通过配置可以开启连接失败后重连机制

yml 复制代码
spring:
  rabbitmq:
    connection-timeout: 1s              # 设置MQ的连接超时时间
    template:
      retry:
        enabled: true                   # 是否开启超时重试机制
        initial-interval: 1000ms        # 失败后的初始等待时间
        multiplier: 1                   # 失败后下次的等待时长倍数
        max-attempts: 3                 # 最大重试次数

生产者确认:MQ由publisher Confirm和Publisher Return两种确认机制,开启确认机制后,在MQ成功收到消息后会返回确认消息给生产者,返回的结果有一下几种情况:

  1. 消息投递到了MQ,但是路由失败,此时会通过Publisher Return返回路由异常原因,然后返回ACK,告知投递成功
  2. 临时消息投递到MQ,并且入队成功,返回ACK,告知投递成功
  3. 持久消息投递到了MQ,并且入队完成持久化,返回ACK,告知ACK投递成功
  4. 其他情况都会返回NACK告知投递失败
yml 复制代码
spring:
  rabbitmq:
    publisher-confirm-type: correlated  # 开启public confirm机制
    publisher-returns: true             # 开启return机制

publisher-confirm-typ有三种模式:

  • none:关闭confirm机制
  • simple:同步阻塞等待MQ的回执信息
  • correlated :MQ异步回调方式,返回回执消息

ReturnCallback

java 复制代码
@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // 获取RabbitTemplate
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);

        // 设置ReturnCallback,每个获取RabbitTemplate只能配置一个设置ReturnCallback
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
            @Override
            public void returnedMessage(ReturnedMessage returnedMessage) {
                log.debug("受到消息的return callback",returnedMessage.getRoutingKey(),returnedMessage.getMessage(),
                        returnedMessage.getExchange(),returnedMessage.getReplyCode(),returnedMessage.getReplyText());
            }
        });
    }
}

ConfirmCallback

java 复制代码
public void testConfirmCallback() {
        // 1. 创建CorrelationData对象(包含唯一ID)
        CorrelationData cd = new CorrelationData(UUID.randomUUID().toString());

        // 2. 添加ConfirmCallback回调
        cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
            @Override
            public void onFailure(Throwable ex) {
                // 消息回调失败时的处理
                log.error("消息回调失败", ex);
            }

            @Override
            public void onSuccess(CorrelationData.Confirm result) {
                // 收到Broker确认时的处理
                log.debug("收到confirm callback回执");

                if (result.isAck()) {
                    // 消息发送成功(Broker已确认)
                    log.debug("消息发送成功,收到ack");
                } else {
                    // 消息发送失败(Broker拒绝)
                    log.error("消息发送失败,收到nack,原因:{}", result.getReason());
                }
            }
        });
        rabbitTemplate.convertAndSend("hhh","red","gello",cd);
    }

MQ的可靠性

在默认情况下,Rabbit MQ会将接收到的信息保存在内存中以降低消息收发的延迟,这会导致两个问题:

  1. 一旦MQ宕机,内存中的消息会丢失
  2. 内存空间存在上限,当消费者故障或处理过慢时,会导致消息积压,引发MQ阻塞

解决方法:

  1. 数据持久化
  2. LazyQueue(常用)

数据持久化

Rabbit MQ数据持久化的三个方面:

交换机持久化

队列持久化

消息持久化:将消息同时写到磁盘和内存中

spring默认将交换机和队列设置会持久化的

LazyQueue(惰性队列)

接收到消息后直接存入磁盘而非内存(内存中只保留最近的消息)

消费者要消费消息时才会从磁盘中读取并加载到内存

支持数百万条的消息存储

消费者可靠性

为了确认消费者是否成功处理消息,Rabbit MQ提供了消费者确认机制。

当消费者处理消息结束后,应该向Rabbit MQ发送一个回执,告诉RabbitMQ自己消息处理状态,回执有三种:

  1. ack:成功处理消息,RabbitMQ从队列中删除该消息
  2. nack:消息处理失败,Rabbit MQ需要再次投递信息
  3. reject:消息处理失败并拒绝该消息,Rabbit MQ从队列中删除该消息

SpringAMQP已经实现了消息确认功能。并允许我们通过配置文件,选择ACK处理方式,有三种方式:

  1. none:不处理,即消息投递给消费者后立即ack,消息会立刻从MQ中删除,非常不安全,不建议使用
  2. manual:手动模式。需要自己在业务代码中调用api,发送ack或reject,存在业务入侵,更灵活
  3. auto:自动模式。SpringAMQP利用AOP对我们的消息处理逻辑做了环绕增强,当业务正常执行时则自动返回ack,当业务出现异常时,会根据异常判断返回不同结果
  • 如果时业务异常,自动返回nack
  • 如果时消息处理或发生异常,会自动返回reject

开启auto模式

yml 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1
        acknowledge-mode: auto

失败重试机制:当消费者出现异常后,消息会不断requeue(重新入队),再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的消息处理飙升,带来不必要的压力

我们可以利用spring的retry机制,再消费者出现异常时利用本地重试,而不是无限的requeue到mq队列

yml 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1                     # 每次取一条
        acknowledge-mode: auto
        retry:
          multiplier: 1                 # 下次失败的等待时长背书
          max-attempts: 3               # 最大重试次数,重试三次后将雄安锡丢掉
          stateless: true               # true无状态;false有状态。如果业务中包含事务,选择false

开启重试模式后,重试次数耗尽如果消息依然失败,则需要有MessageRecoverer接口来处理,她包含三种不同的实现:

  • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认为这种方式
  • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
  • RepublishMessageRecoverer:重试耗尽后,将失败的消息投递到指定的交换机

业务幂等性

业务幂等性:保证重复提交一个业务时只会执行一次

方法一:给每个消息都设置一个唯一id,利用id区分是否是重复消息

  1. 每一条消息都生成一个唯一id和消息一起投递给消费者
  2. 消费者接收到消息后处理自己的业务,业务处理成功后将消息ID保存到数据库
  3. 如果下次又收到相同消息,取数据库查询判断是否存在

方法二:结合业务逻辑,基于业务本身做判断

延迟消息

延迟消息:生产者发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间之后才收到消息

延迟任务:设置在一定时间之后才执行的任务

死信交换机

死信(Dead Letter):是指在消息队列系统中那些无法被正常消费的消息。

死信队列(Dead Letter Queue, DLQ):当消息无法被正常处理时,也就是死信,可以将这些死信发送到一个专门的队列中,以便于后续检查和处理

当一个队列中的消息满足下列情况之一时,就会成为死信:

  • 消费者使用basic.reject或者basic.nack声明消费失败,并且消息的requeue参数设置为false
  • 消息时一个过期消息(达到了队列或消息本身设置的过期时间),超时无人消费
  • 要投递的队列消息堆积满了,最早的消息可能称为死信

如果队列通过dead-letter-exchange属性指定了一个交换机,那么该队列中的死信就回投递到这个交换机中。这个交换机称为死信交换机

延迟消息插件:RabbitMQ推出了一个插件,原生支持延迟消息功能。该插件的原理是设计了一种支持延迟消息功能的交换机,当消息投递到交换机后可以暂存一定时间,到期后再投递到队列

设置30分钟后检测订单支付状态实现非常简单,存在两个问题

  1. 如果并发较高,30分钟可能堆积消息过多,对MQ压力很大
  2. 大多数订单再下单后1分钟内就回支付,但是MQ内等待30分钟,浪费资源
相关推荐
初次攀爬者13 小时前
RabbitMQ的消息模式和高级特性
后端·消息队列·rabbitmq
初次攀爬者3 天前
ZooKeeper 实现分布式锁的两种方式
分布式·后端·zookeeper
让我上个超影吧4 天前
消息队列——RabbitMQ(高级)
java·rabbitmq
塔中妖4 天前
Windows 安装 RabbitMQ 详细教程(含 Erlang 环境配置)
windows·rabbitmq·erlang
断手当码农4 天前
Redis 实现分布式锁的三种方式
数据库·redis·分布式
初次攀爬者4 天前
Redis分布式锁实现的三种方式-基于setnx,lua脚本和Redisson
redis·分布式·后端
业精于勤_荒于稀4 天前
物流订单系统99.99%可用性全链路容灾体系落地操作手册
分布式
Ronin3054 天前
信道管理模块和异步线程模块
开发语言·c++·rabbitmq·异步线程·信道管理
Asher05094 天前
Hadoop核心技术与实战指南
大数据·hadoop·分布式
凉凉的知识库4 天前
Go中的零值与空值,你搞懂了么?
分布式·面试·go