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分钟,浪费资源
相关推荐
奥尔特星云大使4 小时前
MySQL分布式架构:MyCat详解
数据库·分布式·mysql·mycat·高可用
安当加密8 小时前
智能合约在分布式密钥管理系统中的应用
分布式·智能合约
失散138 小时前
分布式专题——41 RocketMQ集群高级特性
java·分布式·架构·rocketmq
失散138 小时前
分布式专题——42 MQ常见问题梳理
java·分布式·架构
安当加密8 小时前
基于区块链的分布式密钥管理系统:构建去中心化、高可信的密码基础设施
分布式·去中心化·区块链
亿牛云爬虫专家8 小时前
优化分布式采集的数据同步:一致性、去重与冲突解决的那些坑与招
分布式·爬虫·数据采集·爬虫代理·代理ip·数据同步·房地产
n8n8 小时前
RocketMQ 与 RabbitMQ 全面对比:架构、性能与适用场景解析
rabbitmq·rocketmq
心随雨下10 小时前
Redis中Geospatial 实际应用指南
数据库·redis·分布式·缓存
想你依然心痛13 小时前
Spark大数据分析与实战笔记(第六章 Kafka分布式发布订阅消息系统-01)
笔记·分布式·spark