RabbitMQ

在这里插入图片描述
在这里插入图片描述

RabbitMQ

一. 简介

MQ(MessageQueue)消息队列,一个队列(是一个逻辑上的存在)中存放了很多的消息,在同一个队列中这些消息是有序的。它的作用有以下几点:

  1. 削峰填谷;
  2. 系统间解耦;
  3. 控制订单的超时;

常用的MQ框架:ActiveMQ、ZeroMQ、QMQ(去哪儿网)、RabbitMQ、RocketMQ(阿里巴巴)、Kafka、Pulsar.

二. RabbitMQ的安装

docker run -p 15672:15672 -p 5672:5672 -d rabbitmq:3.9-management

说明:15672是rabbitmq提供的web服务器的接口;5672是rabbitmq的端口,在浏览器中输入:

http://ip:15672,使用 guest/guest 访问即可

三. RabbitMQ的运行原理

  1. RabbitMQ的一个服务,我们习惯性的将其称作为一个 Broker(所有的MQ都是这么来取名一个MQ的服务器的)
  2. RabbitMQ最核心就是Queue, 消息最终都是放到 Queue中的,每个队列都要有名字;
  3. 消息要达到队列必须要经过交换机,队列必须要通过binding_key绑定到交换机;
  4. 发送消息的时候,每个消息要指定 routing_key,交换机就根据routing_key将消息分发到对应的队列中。
  5. 消息的消费者,直接对应到队列。

MQ的出现就是为了解决上下游的处理速度不均衡问题的。

四. RabbitMQ的5种工作模型

4.1 简单模式

简单模型,一个消息的生产者对应着一个消息的消费者,所采用的交换机使用RabbitMQ提供的默认交换机。

public class Producer {
    public static void main(String[] args) throws Exception{
       Connection connection = ConnectionUtils.getConnection();

        // 获取一个通道
        Channel channel = connection.createChannel();

        /**
         * basicPublish 是发布消息:
         *    第一个参数是:交换机的名字,如果是空字符串,那么使用的是 RabbitMQ自动一个默认交换机
         *    第二个参数是: routing_key;
         *    第三个参数是:消息的属性:后面讲
         *    第四个参数是:具体的消息
         */
        channel.basicPublish("", "first-queue", null, "Hello RabbitMQ".getBytes(StandardCharsets.UTF_8));
        System.out.println("消息发送完毕");
    }
}
public class Consumer {
    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();
        /**
         * 第一个参数:队列的名称
         * 第二个参数:队列是否持久化, true就是队列持久化,就是写入到磁盘中;false表示队列只存在于内容中,rabbitmq挂了,队列没了;都设置true
         * 第三个参数:队列是否为排他队列,就是队列只作用与当前连接断掉了,队列就没了; 都设置为 false
         * 第四个参数:表示是否自动删除, 如果队列中的消息都被消费过(不管是否确认),那么连接断开之后,那么队列就会删除;都设置为false
         * 第五个参数:队里属性;
         *
         * 如果声明了一个队列,并且没有指定绑定到哪个交换机,那么这个队列会绑定到默认交换机,并且它的 binding_key 就是队列的名称
         */
        channel.queueDeclare("first-queue", true, false, false, null);// 声明一个队列

        /**
         * 第一个参数是队列的名词,表示当前想消费哪个队列中的消息;
         * 第二个参数如果为true, 就表示自动确认,那么队列收到消费者的确认之后,就会将消息删除掉; 往后我们都是自动确认;
         * 第三个参数就是处理消息的。
         */
        channel.basicConsume("first-queue", true, new DefaultConsumer(channel){
            // 处理消息, 最后一个参数就是我们的消息
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println(new String(body, Charset.defaultCharset()));

                /**
                System.out.println(envelope.getDeliveryTag());
//                // 自己手动确认,第一个参数表示消息在MQ中的编号;第二个表示是否确认多个
                channel.basicAck(envelope.getDeliveryTag(), false);
                 */
            }
        });
    }
}
4.2 工作模式

工作模型就是相对与简单模型而言,就是多了一个消费者而已。

4.3 发布订阅模型(fanout模型)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s3SAJLrd-1680076110918)(images/python-three.png)]

发布订阅模型,无关路由键,发送到对应交换机的消息会到达所有绑定到该交换机的队列上。

4.4 直连模型(routing 模型)

RabbitMQ的直连模型是最标准的一种工作模型。

4.5 topic模式

fanout是到达所有的队列;direct只能到达某个队列;如果想让消息到达部分队列,那么就使用 topic模型。topic模型中有两个特殊符号:

  1. #,表示0到多个;
  2. *,表示有且只有一个;

五. RabbitMQ与spring整合

第一步,引入依赖:

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

第二步,引入该依赖之后,会在IOC容器中存在一个 RabbitTemplate 这样一个 bean,用来发送消息的;

5.1 简单模型与工作模型
// 简单和工作模型,不需要指定交换机
@RabbitListener(queuesToDeclare = @Queue(name = "work-queue"))
public void consume(String msg) throws InterruptedException {
    System.out.println("One: " + msg + "; " + new Date());
}
5.2 fanout模型
@RabbitListener(bindings = @QueueBinding(
    exchange = @Exchange(name = "fanout-exchange", type = ExchangeTypes.FANOUT),
    value = @Queue("error-queue")
))
public void consume(String msg) {
        System.out.println("Error: " + msg);
    }
}
5.3 direct模型

RabbitMQ的交换机默认类型就是 direct 模型

@RabbitListener(bindings = @QueueBinding(
    exchange = @Exchange(name = "direct-exchange", type = ExchangeTypes.DIRECT),  // 交换的类型默认是 direct模式,可以不用写
    value = @Queue("error-queue"),
    key = {"error", "fatal"}
))
public void consume(String msg) {
    System.out.println("Error: " + msg);
    // insert user
}
5.4 topic模型
@RabbitListener(bindings = @QueueBinding(
    exchange = @Exchange(name = "topic-exchange", type = ExchangeTypes.TOPIC),
    value = @Queue("company-queue"),
    key = "company.#"
))
public void consume(String msg) {
    System.out.println("Company: " + msg);
}
@RabbitListener(bindings = @QueueBinding(
    exchange = @Exchange(name = "topic-exchange", type = ExchangeTypes.TOPIC),
    value = @Queue("java-queue"),
    key = "company.java.*"
))
public void consume(String msg) {
    System.out.println("Java: " + msg);
}
5.5 springboot整合MQ的消息确认方式

springboot在整合MQ的时候,使用的是手动确认的方式。当消息的消费方执行完方法之后,如果没有抛出异常,发送一个 ack 的确认,MQ收到确认之后,会删除对应的消息;如果方法执行抛出了异常,那么就不会发送ack确认,会重新拉去MQ中消息。消息的确认是一条条确认的。在消息消费的方法中,不能捕获异常。

5.6 消息的抓取以及重试机制

在消费消息的时候,默认每次抓取 250 条数据,这个要依据实际情况来配置每次抓取的数据量;

因为消息在消费失败时候,会反复的去抓取数据,这会极大的影响MQ的性能,所以需要配置重试的机制;

spring: 
  rabbitmq:
    host: 192.168.50.53
    port: 5672
    username: admin
    password: admin
    listener:
      simple:
        # 表示每次从 Queue中抓取几条数据
        prefetch: 5
        # retry是重试的意思
        retry:
          # 开启重试机制,如果一旦开启,在达到重试的次数之后,如果消息还是没有被消费成功,那么消息就会丢失;但是可以
          # 将其放入到死信队列。
          enabled: true
          # 初始化时间间隔
          initial-interval: 3000
          # 最大时间间隔
          max-interval: 60000
          # 最大尝试的次数
          max-attempts: 4
          # 重试时间的乘法因此, 第一次间隔3s, 第二次间隔9秒,第三次间隔27s
          multiplier: 3

六. 消息的重复消费

​ 消息的消费方和MQ服务宕机的情况下,某个已经消费过的消息,没有及时的发送 ack确认,这个消息还存在于队列中,当服务恢复之后再次消费该消息,导致消息的重复消费。解决的方式是两点:

  1. 每个消息必须要有一个唯一值;
  2. 消息的消费方做消息的幂等设计;
@Component
@Transactional
public class Reconsume {
    @Resource
    private MqTestMapper mapper;
    @Resource
    private MsgInfoMapper msgInfoMapper;

    @RabbitListener(bindings = @QueueBinding(
            exchange = @Exchange(name = "other-exchange"),  // 交换的类型默认是 direct模式,可以不用写
            value = @Queue("user-queue"),
            key = "user"
    ))
    public void consume(Message message) throws InterruptedException {
        String correlationId = message.getMessageProperties().getCorrelationId();
//        String messageKey = RABBITMQ_MESSAGE_KEY_PREFIX + correlationId;

        QueryWrapper qw = new QueryWrapper();
        qw.eq("msg_id", correlationId);

        Object msgInfo = msgInfoMapper.selectOne(qw);

        if(null == msgInfo) {
            String msg = new String(message.getBody(), Charset.defaultCharset());
            MqTest mqTest = JSONObject.parseObject(msg, MqTest.class);

            mapper.insert(mqTest);  // 业务操作,可能有很多代码
            //为了解决消息的重复消费问题的
            msgInfoMapper.insert(MsgInfo.builder().msgId(correlationId).build());
        }
    }
}

七. 死信队列

私信队列它依然是一个队列,只是它的作用决定我们将其称作一个死信队列;死信队列我们不会直接往这个队列中投递消息。

public class DeadLetterProducer {
    private static final String DEAD_LETTER_QUEUE = "dead_letter_queue";
    private static final String DEAD_LETTER_ROUTING_KEY = "dead_letter_routing_key";
    private static final String DEAD_LETTER_EXCHANGE = "dead_letter_exchange";


    private static final String COMMON_EXCHANGE = "common_exchange";
    private static final String COMMON_QUEUE = "common_queue";
    private static final String COMMON_ROUTING_KEY = "common_routing_key";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        Channel channel = connection.createChannel();

        // 创建一个死信队列
        channel.queueDeclare(DEAD_LETTER_QUEUE, true, false, false, null);
        // 创建一个死信交换机
        channel.exchangeDeclare(DEAD_LETTER_EXCHANGE, BuiltinExchangeType.DIRECT);
        // 将死信队列绑定到死信交换机上
        channel.queueBind(DEAD_LETTER_QUEUE, DEAD_LETTER_EXCHANGE, DEAD_LETTER_ROUTING_KEY);


        // 创建一个常规的交换机
        channel.exchangeDeclare(COMMON_EXCHANGE, BuiltinExchangeType.DIRECT);
        Map<String, Object> params = ImmutableMap.of(
                "x-dead-letter-exchange", DEAD_LETTER_EXCHANGE,
                "x-dead-letter-routing-key", DEAD_LETTER_ROUTING_KEY
        );
        // 创建一个常规队列
        channel.queueDeclare(COMMON_QUEUE, true, false, false, params);
        // 将常规队列绑定到常规交换机上
        channel.queueBind(COMMON_QUEUE, COMMON_EXCHANGE, COMMON_ROUTING_KEY);

        AMQP.BasicProperties bp = new AMQP.BasicProperties.Builder()
                .deliveryMode(2)  // 2表示持久化
                .expiration("15000")  // 表示消息在15s之内没有被消费,就自动进入到死信队列
                .build();


        channel.basicPublish(COMMON_EXCHANGE, COMMON_ROUTING_KEY, bp,"消息".getBytes(StandardCharsets.UTF_8));
    }
}
7.1 死信队列的应用场景
  1. 创建订单的时候,将订单投递到队列中中,设置一个超时时间,但是不要消费该队列中的消息;
  2. 给队列设置死信队列;
  3. 当订单超时后,消息会自动达到死信队列,在死信队列中用一个消费者;
  4. 死信队列的消费者,在获取到订单之后,判断订单是否支付,如果支付了啥都不做;如果没之后,取消订单;

八. RabbitMQ的消息的可靠性投递

  1. 在投递消息之前,将消息在本地存储一份;
  2. 开头投递消息;
  3. 如果MQ通过消息确认机制告知我们,如果成功到达队列就将消息删除;如果没有成功到达队列,就啥也不干;
  4. 引入消息补偿系统,其实就是通过定时任务,扫描消息表,进行消息的重投;

九. 接口的幂等设计

其他

  1. 系统永远以管理身份运行:https://baijiahao.baidu.com/s?id=1725635393301181756&wfr=spider&for=pc

bitMQ的消息的可靠性投递

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值