学新通技术网

RabbitMQ快速入门

juejin 12 1
RabbitMQ快速入门

RPC

进程间通信(IPC,Inter-Process Communication),指至少两个进程或线程间传送数据或信号的一些技术或方法。进程是计算机系统分配资源的最小单位。每个进程都有自己的一部分独立的系统资源,彼此是隔离的。为了能使不同的进程互相访问资源并进行协调工作,才有了进程间通信。这些进程可以运行在同一计算机上或网络连接的不同计算机上。 进程间通信技术包括消息传递、同步、共享内存和远程过程调用。 IPC是一种标准的Unix通信机制。

有两种类型的进程间通信(IPC)。

  • 本地过程调用(LPC)LPC用在多任务操作系统中,使得同时运行的任务能互相会话。这些任务共享内存空间使任务同步和互相发送信息。
  • 远程过程调用(RPC)RPC类似于LPC,只是在网上工作。RPC开始是出现在Sun微系统公司和HP公司的运行UNIX操作系统的计算机中。

为什么RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如比如不同的系统间的通讯,甚至不同的组织间的通讯。由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用

RPC的核心并不在于使用什么协议。RPC的目的是让你在本地调用远程的方法,而对你来说这个调用是透明的,你并不知道这个调用的方法是部署哪里。通过RPC能解耦服务,这才是使用RPC的真正目的。RPC的原理主要用到了动态代理模式,至于http协议,只是传输协议而已。简单的实现可以参考spring remoting,复杂的实现可以参考dubbo。

简单的说,

  • RPC就是从一台机器(客户端)上通过参数传递的方式调用另一台机器(服务器)上的一个函数或方法(可以统称为服务)并得到返回的结果。
  • RPC 会隐藏底层的通讯细节(不需要直接处理Socket通讯或Http通讯) RPC 是一个请求响应模型。
  • 客户端发起请求,服务器返回响应(类似于Http的工作方式) RPC 在使用形式上像调用本地函数(或方法)一样去调用远程的函数(或方法)。

集群

这个就不在这里解释了,参考

www.cnblogs.com/qinlulu/p/1…

安装RabbitMq

  • 安装教程和入门看下面:

超详细的RabbitMQ入门,看这篇就够了!-阿里云开发者社区 (aliyun.com)

  • RabbititMq 安装包的下载

image-20220615161213251

  • 登录:
    guest
    guest
        
       http:localhost:15672
    

启动 RabbitMq

  • 在当前目录 (sbin) 下执行命令

image-20220617183652678

rabbitmqctl status	//查看当前状态
rabbitmq-plugins enable rabbitmq_management	//开启Web插件
rabbitmq-server start	//启动服务
rabbitmq-server stop	//停止服务
rabbitmq-server restart	//重启服务
    
      http:localhost:15672

第一个程序 Hello-world

RabbitMQ的概念 RabbitMQ是一个消息中间件:它接收并转发消息。你可以把它 当作一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会将你的快递送到收件人那里,按照这种逻辑RabbitMQ就是一个快递站。RabbitMQ接收,存储和转发消息数据。

四大核心概念

  • 生产者

产生数据发送消息的程序是生产者

  • 交换机

交换机是 RabbitMQ 非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息 推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推 送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定

  • 队列

队列是 RabbitMQ 内部使用的一种数据结构,尽管消息流经 RabbitMQ 和应用程序,但它们只能存 储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。许多生产者可 以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。这就是我们使用队列的方式

  • 消费者

消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。请注意生产者,消费 者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者

原理名词解释

在这里插入图片描述

  • Broker:接收和分发消息的应用,RabbitMQ Server就是Message Broker

  • Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个 vhost,每个用户在自己的 vhost 创建 exchange/queue 等

  • Connection:publisher/consumer 和 broker 之间的 TCP 连接

  • Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了 channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的Connection 极大减少了操作系统建立 TCP connection 的开销

  • Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)

  • Queue:消息最终被送到这里等待 consumer 取走

  • Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key,Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据

routingKey 作用
  • 指定路由规则

  • 绑定交换机和队列直接的关系

  • 对于消息发布者而言它只负责把消息发布出去,甚至它也不知道消息是发到哪个queue,消息通过exchange到达queue,exchange的职责非常简单,就是一边接收发布者的消息一边把这些消息推到queue中。

    而exchange是怎么知道消息应该推到哪个queue呢,这就要通过绑定queue与exchange时的routingkey了,通过代码进行绑定并且指定routingkey,下面有一张关系图,p(发布者) —> x(exchange) bindding (绑定关系也就是我们的routingkey) 红色代表着queue

  • 在这里插入图片描述

  • (91条消息) rabbitmq的routingkey的作用_张超帅的博客-CSDN博客_mq routingkey

例如:

topic 交换机的案例 , Map 里面的 key 必须含有下面的路由规则才能进行

image-20220726210150817

image-20220726210212206

第一个程序

消费者代码

public class Custom {

    private static final String QUEUE_NAME="hello-world";
    public static void main(String[] args) throws IOException, TimeoutException {
        // 创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
//        factory.setHost("182.92.234.71");
        factory.setUsername("guest");
        factory.setPassword("guest");
        // 创建连接
        Connection connection = factory.newConnection();
        // 从连接中获取信道
        Channel channel = connection.createChannel();
        System.out.println("等待接收消息....");

        //推送的消息如何进行消费的接口回调
        DeliverCallback deliverCallback=(consumerTag, delivery)->{
            String message= new String(delivery.getBody());
            System.out.println("消费者-"+message);
        };

        //取消消费的一个回调接口 如在消费的时候队列被删除掉了
        CancelCallback cancelCallback=(consumerTag)->{
            System.out.println("消息消费被中断");
        };

        /**
         * 消费者消费消息
         * 1.消费哪个队列
         * 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答3
         * 3.推送的消息如何进行消费
         * 4.消费者未成功消费的回调
         */
        channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
    }
}

生产者代码
public class Producer {

    private static final String QUEUE_NAME="hello-world";
    public static void main(String[] args) throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
//        factory.setHost("http:localhost:15672");
        factory.setUsername("guest");
        factory.setPassword("guest");
        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();
        /**
         * 生成一个队列,参数含义
         * 1.队列名称
         * 2.队列里面的消息是否持久化 默认消息存储在内存中
         * 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
         * 4.是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true 自动删除
         * 5.其他参数
         */
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        String message="hello world";
        /**
         * 发送一个消息,参数含义
         * 1.发送到那个交换机
         * 2.路由的 key 是哪个
         * 3.其他的参数信息
         * 4.发送消息的消息体
         */
        channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
        System.out.println("消息发送完毕---");
    }
}

消息应答概念

  • 自动应答

消息发送后立即被认为已经传送成功,这种模式需要在高吞吐量和数据传输安全性方面做权衡,因为这种模式如果消息在接收到之前,消费者那边出现连接或者 channel 关闭,那么消息就丢失了,当然另一方面这种模式消费者那边可以传递过载的消息,没有对传递的消息数量进行限制,当然这样有可能使得消费者这边由于接收太多还来不及处理的消息,导致这些消息的积压,最终使得内存耗尽,最终这些消费者线程被操作系统杀死,所以这种模式仅适用在消费者可以高效并以某种速率能够处理这些消息的情况下使用。

接收到消息就应答,当后续方法出错就导致消息丢失,所以自动应答并不安全

  • 手动应答

如果消费者由于某些原因失去连接(其通道已关闭,连接已关闭或TCP 连接丢失),导致消息未发送 ACK 确认,RabbitMQ 将了解到消息未完全处理,并将对其重新排队。如果此时其他消费者可以处理,它将很快将其重新分发给另一个消费者。这样,即使某个消费者偶尔死亡,也可以确保不会丢失任何消息。

手动应答代码

  • 实现思想
生产者发送信息,模拟实现:
消费者一个快一点,一个慢一点(采用睡眠实现)
然后关掉慢一点,快一点的应该处理那个中断的信息
  • 生产者代码
public class 生产者 {
    private static final String QUEUE_NAME="Manual_response";
    public static void main(String[] args) throws Exception{
        Channel channel = RabbitMqUtils.getChannel();
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        Scanner scanner = new Scanner(System.in);
        System.out.println("生产者开始发送消息---");
        while (scanner.hasNext()){
            String s=scanner.next();
            channel.basicPublish("",QUEUE_NAME,null,s.getBytes(StandardCharsets.UTF_8));
        }
    }
}
  • 消费者代码
  • 注意那个睡眠时间即可
public class Work03 {
	private static final String QUEUE_NAME="Manual_response";
	
	public static void main(String[] args) throws Exception {
		Channel channel = RabbitMqUtils.getChannel();
		System.out.println("C1 等待接收消息处理时间较短");
		//消息消费的时候如何处理消息
		DeliverCallback deliverCallback=(consumerTag, delivery)->{
			String message= new String(delivery.getBody());
			try {
				Thread.sleep(1);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("接收到消息:"+message);
			/**
			* 参数:
			* 1.消息标记 tag
			* 2.是否批量应答未应答消息
			*/
			channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
		};

		//采用手动应答
		boolean autoAck=false;
		channel.basicConsume(QUEUE_NAME,autoAck,deliverCallback,(consumerTag)->{
			System.out.println(consumerTag+"消费者取消消费接口回调逻辑");
		});
	}
}

持久化的概念

持久化

刚刚我们已经看到了如何处理任务不丢失的情况,但是如何保障当 RabbitMQ 服务停掉以后消息生产者发送过来的消息不丢失。默认情况下 RabbitMQ 退出或由于某种原因崩溃时,它忽视队列和消息,除非告知它不要这样做。确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久化。

队列持久化

非持久化队列,rabbitmq 重启该队列就会被删除掉

// 持久化队列

Boolean durable=true;
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
消息持久化

要想让消息实现持久化需要在消息生产者修改代码,MessageProperties.PERSISTENT_TEXT_PLAIN 添加这个属性

// 设置持久化消息,将信息保存在磁盘上

channel.basicPublish("",TASK_QUEUE_NAME,MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes("UTF-8"));

将消息标记为持久化并不能完全保证不会丢失消息。 尽管它告诉 RabbitMQ 将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候,但是还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。

不公平分发

对于消费者,能者多劳

int prefetchCount=1;
channel.basicQos(prefetchCount);

预取值

指定消费者获取几条消息,不管消费者效率如何,初始都会按照预取值来分配

预取值指消息堆积的数量

发布确认原理

作用: 保证消息的持久化,因为怕消息持久化还没到磁盘,MQ 就挂掉了

做到真正的消息不丢失: 1.持久化队列(生产者端设置) 2.持久化消息(生产者端设置) 3.发布确认(生产者端设置)

原理

生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理

confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息

开启确认

默认不开启

Channel channel = connection.createChannel();
channel.confirmSelect();

开启发布确认的方法

单个发布确认

发布一条,确认一条,同步确认发布的方式 缺点是发布速度慢,会有阻塞

批量发布确认

与单个发布确认相比,效率提高了 缺点是当发生问题时,不能精准定位问题

异步确认

image-20220618175343205

三种确认方法完整代码
public class Test {
    private static final String QUEUE_NAME="confirm";
    private static final int COUNT=10;
    public static void main(String[] args) throws Exception {
//        singleConfirm();
//        batchConfirm();
        asyncConfirm();

    }

    private static void asyncConfirm() throws Exception {
        long startTime=System.currentTimeMillis();
        Channel channel = RabbitMqUtils.getChannel();
        //        开启确认
        channel.confirmSelect();
        //     线程安全有序的一个哈希表,适用于高并发的情况
        ConcurrentSkipListMap<Long, String> skipListMap = new ConcurrentSkipListMap<Long, String>();
        ConfirmCallback ack=((deliveryTag,multiple)->{
            if (multiple){
                ConcurrentNavigableMap<Long, String> headMap =
                        skipListMap.headMap(deliveryTag);
                headMap.clear();
            }else {
                skipListMap.remove(deliveryTag);
            }
        });
        ConfirmCallback nack=((deliveryTag,multiple)->{

        });
        channel.addConfirmListener(ack,nack);

        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        for (int i = 0; i < COUNT; i++) {
            String msg=i+"";
            channel.basicPublish("",QUEUE_NAME,null,msg.getBytes(StandardCharsets.UTF_8));
            skipListMap.put(channel.getNextPublishSeqNo(),msg);
            System.out.println("测试参数channel的标记"+channel.getNextPublishSeqNo());
        }
        boolean flag = channel.waitForConfirms();
        if (flag){
            System.out.println("消息发送成功");
        }
        long endTime=System.currentTimeMillis();
        System.out.println("async发送"+COUNT+"耗时"+(endTime-startTime));
    }

    //    批量确认
    private static void batchConfirm() throws Exception {
        long startTime=System.currentTimeMillis();
        Channel channel = RabbitMqUtils.getChannel();
        //        开启确认
        channel.confirmSelect();
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);
        for (int i = 0; i < COUNT; i++) {
            String msg=i+"";
            channel.basicPublish("",QUEUE_NAME,null,msg.getBytes(StandardCharsets.UTF_8));
        }
        boolean flag = channel.waitForConfirms();
        if (flag){
            System.out.println("消息发送成功");
        }
        long endTime=System.currentTimeMillis();
        System.out.println("batch发送"+COUNT+"耗时"+(endTime-startTime));
    }

    //    单个发布确认
    private static void singleConfirm() throws Exception{
        long startTime=System.currentTimeMillis();
        Channel channel = RabbitMqUtils.getChannel();
    //        开启确认
        channel.confirmSelect();
        channel.queueDeclare(QUEUE_NAME,true,false,false,null);
        for (int i = 0; i < COUNT; i++) {
            String msg=i+"";
            channel.basicPublish("",QUEUE_NAME,null,msg.getBytes(StandardCharsets.UTF_8));
            boolean flag = channel.waitForConfirms();
            if (flag){
                System.out.println("消息发送成功");
            }
        }
        long endTime=System.currentTimeMillis();
        System.out.println("single发送"+COUNT+"耗时"+(endTime-startTime));
    }
}

如何处理异步未确认消息 最好的解决的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,比如说用 ConcurrentLinkedQueue 这个队列在 confirm callbacks 与发布线程之间进行消息的传递

3种发布确认速度对比

单独发布消息 同步等待确认,简单,但吞吐量非常有限。 批量发布消息 批量同步等待确认,简单,合理的吞吐量,一旦出现问题但很难推断出是那条消息出现了问题。 异步处理 最佳性能和资源使用,在出现错误的情况下可以很好地控制,但是实现起来稍微难些

交换机

交换机的更多详情如下:

RabbitMQ笔记三:四种类型Exchange - 简书 (jianshu.com)

交换机的作用

将消息传达给多个消费者。这种模式 称为 ”发布/订阅”

RabbitMQ 消息传递模型的核心思想是: 生产者生产的消息从不会直接发送到队列。实际上,通常生产者甚至都不知道这些消息传递传递到了哪些队列中

相反,生产者只能将消息发送到交换机(exchange),交换机工作的内容非常简单,一方面它接收来自生产者的消息,另一方面将它们推入队列。交换机必须确切知道如何处理收到的消息。是应该把这些消息放到特定队列还是说把他们到许多队列中还是说应该丢弃它们。这就的由交换机的类型来决定

4种交换机的类型
  • Direct Exchange:将消息中的Routing key与该Exchange关联的所有Binding中的Routing key进行比较,如果相等,则发送到该Binding对应的Queue中,就跟广播差不多

  • Topic Exchange:将消息中的Routing key与该Exchange关联的所有Binding中的Routing key进行对比,如果匹配上了,则发送到该Binding对应的Queue中。

topic 上面讲到direct类型的交换器路由规则是必须完全匹配BindingKey和RoutingKey,但这种严格的匹配方式在很多情况下无法满足实际业务的需求。topic类型的交换器在匹配规则上进行了扩展,它与direct类型的交换器类似,也是将消息路由到BindingKey和RoutingKey相匹配的队列中,但匹配规则略有不同,约定如下:

1.RoutingKey 为一个点号""分隔的字符串,被""号分隔的每一段独立的字符串称为一个单词,如"com.rabbitmq.client"

2.Bindingkey 和 Routingkey一样也是"."分隔的字符串。

3.BindingKey中存在两种特殊宇符串 * 和 #,用于做模糊匹配,其中 * 用于匹配一个单词,"#"用于匹配多个单词(可以是零个)

image-20220924165540857

  • Fanout Exchange:直接将消息转发到所有binding的对应queue中忽略Routing key。
  • Headers Exchange:将消息中的headers与该Exchange相关联的所有Binging中的参数进行匹配,如果匹配上了,则发送到该Binding对应的Queue中。

springboot 整合

问题

如果更改交换机和队列的绑定关系,或者自动创建队列或者交换机失败

解决方法:

  • 删除原来的交换机或者队列

延迟队列

架构图

  • 详细代码看:

image-20220804101319622

image-20220804101343926

思考:

  • 如果不同的需求,需要设置不同的过去时间,上面这样创建肯定是不行的
  • image-20220804170727236

延迟队列的问题

看起来似乎没什么问题,但是在最开始的时候,就介绍过如果使用在消息属性上设置 TTL 的方式,消息可能并不会按时“死亡“,因为** RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行**

问题: 时间短的在后面并不会先执行

解决办法

rabbitmq-plugins enable rabbitmq_delayed_message_exchange
  • 成功:
  • image-20220813110656458

基于死信队列

image-20220813104705015

基于插件

image-20220813104723505

  • 配置类
package com.rabbitmqboot.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author: 雨同我
 * @Description:
 * @DateTime: 2022/8/13 11:11
 **/

@Configuration
public class DelayedQueueConfig {
    public static final String DELAYED_QUEUE_NAME = "delayed.queue";
    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
    public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";

    @Bean("customQueue")
    public Queue delayedCustomQueue() {
        return new Queue(DELAYED_QUEUE_NAME);
    }

    //自定义交换机 我们在这里定义的是一个延迟交换机
    @Bean("customExchange")
    public CustomExchange delayedCustomExchange() {
        Map<String, Object> args = new HashMap<>();
        //自定义交换机的类型
        args.put("x-delayed-type", "direct");
        return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false,
                args);
    }
    @Bean
    public Binding bindingCustomDelayedQueue(@Qualifier("customQueue") Queue queue,
                                       @Qualifier("customExchange") CustomExchange
                                               delayedExchange) {
        return BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
    }
}

  • 生产者
/**
	 * @Description: 延迟队列插件实现
	 * @Author: 雨同我
	 * @DateTime: 2022/8/13 11:33
	*/


	public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
	public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
	@GetMapping("/sendDelayMsg/{message}/{delayTime}")
	public void sendMsg(@PathVariable String message,@PathVariable Integer delayTime) {
		rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY, message,
				correlationData ->{
					correlationData.getMessageProperties().setDelay(delayTime);
					return correlationData;
				});
		log.info(" 当 前 时 间 : {}, 发送一条延迟 {} 毫秒的信息给队列 delayed.queue:{}", new
				Date(),delayTime, message);
	}

  • 消费者

public static final String DELAYED_QUEUE_NAME = "delayed.queue";
@RabbitListener(queues = DELAYED_QUEUE_NAME)
public void receiveDelayedQueue(Message message){
	String msg = new String(message.getBody());
	log.info("当前时间:{},收到延时队列的消息:{}", new Date().toString(), msg);
}
  • 如果创建队列和交换机没有连接起来,可以降低一下 springboot 的版本,或者手动连接

总结

延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。

当然,延时队列还有很多其它选择,比如利用 Java 的 DelayQueue,利用Redis 的 zset,利用 Quartz或者利用 kafka 的时间轮,这些方式各有特点,看需要适用的场景

发布确认高级

详细代码:

在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢?特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢

  • 可以使用缓存进行定时投递

代码架构图

image-20220813165128780

接下来我们通过代码实现以上机制,架构图如下所示:我们要解决问题就是如果图中的交换机或者队列出现问题,应该将消息进行缓存处理,防止消息丢失,具体的实现就是通过生产者的回调接口ConfirmCallback来实现。

回调接口——消息回退,消息确认

参考:(94条消息) RabbitMQ超详细学习笔记(章节清晰+通俗易懂)_Baret-H的博客-CSDN博客

更多结合代码,这里不多多解释了

结论

当回调函数和备用交换机一起使用的时候,备份交换机优先级高。
回调函数也不会显示了

幂等性

问题引入:

所谓幂等性就是指用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。比如用户购买商品后支付后已经扣款成功,但是返回结果时出现网络异常,用户并不知道自己已经付费成功,于是再次点击按钮,此时就进行了第二次扣款,这次的返回结果成功。但是扣了两次用户的钱,这就出现了不满足幂等性,即用户对统一操作发起了一次或多次请求不一致,产生了副作用导致用户被多扣费了。

  • 意思是重复消费

解决方法

  • 指纹码机制:指纹码是按照一定规则,比如时间戳、其他服务给的唯一信息码而拼接出来的唯一标识,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来。然后就利用查询语句进行判断这个指纹码是否存在数据库中,优势就是实现简单,只需要进行拼接即可,然后查询判断是否重复;劣势就是在高并发时,如果是单个数据库会出现写入性能瓶颈,当然也可以采用分库分表提升性能。(不推荐)
  • Redis 原子性:利用 redis 执行 setnx 命令,天然具有幂等性,从而实现不重复消费。(推荐)

优先级队列

使用场景

  • 电商系统中常常会遇到订单催付的场景,比如用户在淘宝下单后,往往会及时将订单信息推送给用户,如果用户在设定的时间内未付款那么就会给用户推送一条短信提醒,这个功能看似很简单,但是当订单量十分大的情况下,商家往往要进行优先级排序,大客户先推送,小客户后推送的。曾经我们的后端系统的解决办法是使用 redis 的 List 做一个简简单单的消息队列来实现定时轮询推送,但这并不能实现一个优先级的场景,所以订单量大了之后我们应该采用 RabbitMQ 进行改造和优化,给以给要发送的消息设置优先级,满足不同场景的需要。

代码实现

手动设置

  • image-20220816114000671

代码编写

  • 生产者

  • package com.mq.优先级队列;
    
    import com.rabbitmq.client.*;
    
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.TimeoutException;
    
    /**
     * @Author: 雨同我
     * @Description:
     * @DateTime: 2022/8/16 11:51
     **/
    public class PriorityQueueProducer {
        private static final String QUEUE_NAME="priorityQueue";
        public static void main(String[] args) throws IOException, TimeoutException {
            ConnectionFactory factory = new ConnectionFactory();
    //        factory.setHost("http:localhost:15672");
            factory.setUsername("guest");
            factory.setPassword("guest");
            Connection connection = factory.newConnection();
            Channel channel = connection.createChannel();
            /**
             * 生成一个队列,参数含义
             * 1.队列名称
             * 2.队列里面的消息是否持久化 默认消息存储在内存中
             * 3.该队列是否只供一个消费者进行消费 是否进行共享 true 可以多个消费者消费
             * 4.是否自动删除 最后一个消费者断开连接以后 该队列是否自动删除 true 自动删除
             * 5.其他参数
             */
    
            Map<String, Object> arguments=new HashMap<>();
    //        设置0-10
            arguments.put("x-max-priority",10);
            channel.queueDeclare(QUEUE_NAME,true,false,false,arguments);
            /**
             * 发送一个消息,参数含义
             * 1.发送到那个交换机
             * 2.路由的 key 是哪个
             * 3.其他的参数信息
             * 4.发送消息的消息体
             */
            for (int i = 0; i < 10; i++) {
                String msg="info"+i;
                if (i==9){
                    AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder().priority(5).build();
                    channel.basicPublish("",QUEUE_NAME,basicProperties,msg.getBytes());
                }else {
                    channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());
                }
            }
            System.out.println("消息发送完毕---");
        }
    }
    
    
  • 消费者

  • package com.mq.优先级队列;
    
    import com.rabbitmq.client.*;
    
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    /**
     * @Author: 雨同我
     * @Description:
     * @DateTime: 2022/8/16 11:51
     **/
    
    public class PriorityQueueCustom {
        private static final String QUEUE_NAME="priorityQueue";
        public static void main(String[] args) throws IOException, TimeoutException {
            // 创建连接工厂
            ConnectionFactory factory = new ConnectionFactory();
    //        factory.setHost("182.92.234.71");
            factory.setUsername("guest");
            factory.setPassword("guest");
            // 创建连接
            Connection connection = factory.newConnection();
            // 从连接中获取信道
            Channel channel = connection.createChannel();
            System.out.println("等待接收消息....");
    
            //推送的消息如何进行消费的接口回调
            DeliverCallback deliverCallback=(consumerTag, delivery)->{
                String message= new String(delivery.getBody());
                System.out.println("消费者-"+message);
            };
    
            //取消消费的一个回调接口 如在消费的时候队列被删除掉了
            CancelCallback cancelCallback=(consumerTag)->{
                System.out.println("消息消费被中断");
            };
    
            /**
             * 消费者消费消息
             * 1.消费哪个队列
             * 2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答3
             * 3.推送的消息如何进行消费
             * 4.消费者未成功消费的回调
             */
    
            channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
    
        }
    }
    
    

 

image-20220816162036139

  • 设置部分
// 1.队列声明的代码中添加优先级参数
Map<String, Object> params = new HashMap();
params.put("x-max-priority", 10);	// 官方允许是0~255之间,此处设置为10,即允许优先级范围从0~10(不要设置过大以免浪费cpu和内存)
channel.queueDeclare("hello", true, false, false, params);

// 2.消息中代码添加优先级(要在队列优先级设置的范围内)
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
channel.basicPublish("",QUEUE_NAME,properties,message.getBytes());

注意:队列需要设置为优先级队列的同时消息也必须设置消息的优先级才能生效,而且消费者需要等待消息全部发送到队列中才去消费因为这样才有机会对消息进行排序。

惰性队列

使用场景

RabbitMQ 从 3.6.0 版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了,我们可以将消息存储在磁盘中,避免占用大量内存。

默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存之中,这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。当 RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ 的开发者们一直在升级相关的算法,但是效果始终不太理想,尤其是在消息量特别大的时候。

  • 意思是,默认情况下,消息存放在内存中,惰性队列存放在磁盘中

使用惰性队列

  • 创建

image-20220816163231941

  • 在队列声明的时候可以通过 “x-queue-mode” 参数来设置队列的模式,取值为 “default” 和 “lazy”。
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args);

内存开销

image-20211201153234314

在发送 1 百万条消息,每条消息大概占 1KB 的情况下,普通队列占用内存是 1.2GB,而惰性队列仅仅占用 1.5MB

本文出至:学新通技术网

标签: