RabbitMQ 中小型企业MQ的不二选择,入门级别教程,适合初学者

  • rabbitmq适合中型企业使用,具有较高的吞吐量,但唯一的缺点是其底层是Erlang语言,源码阅读起来相当麻烦.

Erlang语言安装(rpm版,建议)

  • 下载erlang-21.3-1.el7.x86_64.rpm,如果找不到,可以去尚硅谷里2021版本的rabbitmq的资料里有.
  • rpm -ivh erlang-21.3-1.el7.x86_64.rpm 一键安装.
  • whereis erlang可以查看erlang安装在哪个路径中.
  • 然后进入到该路径,在bin目录下输入erl可以查看erlang版本号,看是否安装成功.

Rabbitmq安装(rpm版,推荐)

  • 下载rabbitmq-server-3.8.8-1.el7.noarch.rpm,这个版本和Erlang版本对应的.
  • rpm -ivh rabbitmq-server-3.8.8-1.el7.noarch.rpm 一键安装.
  • 如果安装不成功,yum install socat -y 安装socat环境.
  • /sbin/service rabbitmq-server start/stop/status/restart 为rabbitmq开启,停止,查看,重启命令.

卸载

  • 比如卸载erlang.
  • whereis erlang查看安装路径.将这个路径下的所有文件都删除了:rm -rf /usr/local/erlang.
  • 然后yum list | grep erlang 查看所有erlang的安装包.
  • 然后yum remove 某某文件,便可以将该文件删除.

Erlang语言的安装(压缩包版本,不建议)

  • 下载Erlang语言的压缩包otp_src_21.3.8.24.tar.gz 这里我选择21.3.8.24的版本
  • 解压该文件 tar -zxvf otp_src_21.3.8.24.tar.gz.
  • 解压完成后,将解压包otp_src_21.3.8.24转移到一个规矩的目录,比如是usr/local/erlang目录中.
  • yum -y install make gcc gcc-c++ kernel-devel m4 ncurses-devel openssl-devel 准备make环境
  • cd usr/local/erlang/otp_src_21.3.8.24 进入该目录
  • 设置安装规则./configure --prefix=/usr/local/erlang --with-ssl --enable-threads --enable-smp-support --enable-kernel-poll --enable-hipe --without-javac
  • make && make install 进行安装
  • vim /etc/profile 进入该文件设置环境变量
#set erlang environment
ERL_PATH=/usr/local/erlang/bin
PATH=$ERL_PATH:$PATH
  • 立马生效source /etc/profile
  • erl检查是否成功

![image.png](https://img-blog.csdnimg.cn/img_convert/da11c9c76319faa8fc778d7f2a5ebf76.png#clientId=u91b4b6b5-17f9-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=ua40de20e&margin=[object Object]&name=image.png&originHeight=74&originWidth=717&originalType=url&ratio=1&rotation=0&showTitle=false&size=20926&status=done&style=none&taskId=u21202468-68f2-4c92-a7f6-d951eab2b33&title=)

  • halt(). 退出检查 注意有.号

Rabbitmq安装(压缩包版,不建议)

  • 官网下载rabbitmq-server-generic-unix-3.8.8.tar.xz,然后tar -xvf rabbitmq_server-3.8.8.tar.xz解压该压缩包.
  • 得到压缩包后 cp -rf rabbitmq_server-3.8.8 /路径 转移到一个文件夹里,然后将当前文件夹下的解压包删除rm -rf rabbitmq-server_3.8.8,若是没有移动,则不用删除.
  • 配置环境变量 vim /etc/profile中

ef64a1b4ae3876c152b46dd99115489.png
注意:若有配置多个不同的环境,变量名字不能一样,要分开.

  • 配置好环境变量后,source /etc/profile 让其立即生效.
  • 然后 /sbin/service rabbitmq-server start 启动rmq的服务.如果报以下错误:0038760b6e0045868a45f85ee9d69c4.png

这说明/usr/lib/rabbitmq/bin/rabbitmq-server 这个文件中一个erl的变量未找到.
这个问题肯定是你erlang的环境变量没有配置好,在profile文件中,erlang的环境变量是要用/usr/local/erlang这个包中bin目录,而不是erlang/otp…/bin这个bin目录.
如果发现路径确实配置的没有问题,但还是会提示这个错误,那边直接将erl这个地址写死就可以了.这个erl地址你可以进入到/usr/local/erlang/bin中查看.确实是有erl这个文件的,将这个文件的目录写进入.如下:
0cd241e1139e8ea544c64e3a3bcea99.png
然后再用同样的命令启动服务,则会成功解决这个错误,如果还不能启动成功,那就不是这个问题.
启动没有报error字样的时候,可以试着用 /sbin/service rabbitmq-server status 查看状态,若是出现active绿色字样,则说明成功.
1cd77df7e6c0d901157c6053b25cf63.png

安装web管理页面

  • 安装之前先关闭rabbitmq.
  • rabbitmq-plugins enable rabbitmq_management 安装.
  • 然后重启.
  • 重启成功后在谷歌浏览器浏览地址:服务器ip:15672 ,出现页面则说明安装成功.
  • web管理页面初始化账户和密码都是guest
  • 但我们没有权限访问初始账户,所以我们得新建立一个账户.
  • rabbitmqctl add_user admin 123 创建一个新账户为admin,密码是123.
  • 设置用户角色为超级管理员,rabbitmqctl set_user_tags admin administrator
  • 设置用户权限:rabbitmqctl set_permissions -p “/” admin “." ".” “.*”
  • 可以查看该用户是否存在 rabbitmqctl list_users
  • 如果存在刚才添加的用户就可以去前台登录了.

Hello World

  • 新建Maven工程
  • 导入依赖
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.8.0</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.6</version>
        </dependency>

生产者代码

    private static final String QUEUE="hello";

    public static void main(String[] args) throws Exception{
//        创建一个连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
//        获取ip,用户
        connectionFactory.setHost("192.168.188.100");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("123");
//        创建连接
        Connection connection = connectionFactory.newConnection();
//        获取信道
        Channel channel = connection.createChannel();
//        连接队列,在更复杂的连接中,队列前应该有个交换机
        /*
        * 方法参数
        * 1.队列名称
        * 2.队列是否持久化(存在硬盘),非持久化存放在内存
        * 3.队列消息是否进行共享(是否只供一个消费者消费)
        * 4.是否自动删除 最后一个消费者消费后,是否删除消息
        * 5.其他参数 (死信队列,延迟消息)
        * */
        channel.queueDeclare(QUEUE,false,false,false,null);
//        消息
        String message="hello world";
        /*
        * 方法参数
        * 1.发送给哪个交换机,本次程序没有交换机,可以提高空字符串
        * 2.路由的key值,本次默认为队列名称
        * 3.其他参数信息
        * 4.发送的消息,传入字节
        * */
        channel.basicPublish("",QUEUE,null,message.getBytes());
    }
  • 成功后,前台页面会出现

image.png

消费者代码

    private static final String QUEUE="hello";

    public static void main(String[] args) throws Exception{
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.188.100");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("123");
        Connection connection = connectionFactory.newConnection();
        Channel channel = connection.createChannel();
        /*
        * 接收消息方法参数
        * 1.队列名称
        * 2.是否自动应答
        * 3.成功回调
        * 4.失败回调
        * */
//        回调函数
        DeliverCallback deliverCallback=(consumerTag,message)->{
            System.out.println(new String(message.getBody()));
        };
        CancelCallback cancelCallback=(consumerTag)->{
            System.out.println("消息中断");
        };
        channel.basicConsume(QUEUE,true,deliverCallback,cancelCallback);
    }
  • 成功后,前台显示

image.png

工作队列

  • 前面的入门案例用的队列是普通队列,而工作队列就用来处理大量消息.
  • 多个工作线程处理这一批消息,每个线程处理完一个消息便轮到下一个线程处理另一条消息,每条消息只能被处理一次.

  • 创建信道
    public static Channel getChannel() throws Exception{
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.188.100");
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("123");
        Connection connection = connectionFactory.newConnection();
        return connection.createChannel();
    }
  • 新建生产者
	private static final String QUEUE="hello";
	public static void main(String[] args) throws Exception{
        Channel channel = RabbitmqUtil.getChannel();
        /*
        * 1.是否持久化
        * 2.是否共享
        * 3.是否自动删除
        * 4.其他消息,死信队列,延迟消息
        * */
        channel.queueDeclare(QUEUE,false,false,false,null);
        String message="这是一个消息5";
        channel.basicPublish("",QUEUE,null,message.getBytes());
    }
  • 新建消费者
    private static final String QUEUE="hello";
    public static void main(String[] args) throws Exception{
        Channel channel = RabbitmqUtil.getChannel();
        DeliverCallback deliverCallback=(consumerTag,message)->{
            System.out.println(new String(message.getBody()));
        };

        CancelCallback cancelCallback=(consumerTag)->{
            System.out.println(consumerTag+"->"+"消息被取消");
        };

        channel.basicConsume(QUEUE,true,deliverCallback,cancelCallback);
    }
  • 测试:启动两次消费者,创建出两个线程,然后修改信息内容,启动多次生产者,观察两个消费者线程是否轮询接收消息.

消息应答

概念

  • 如果有多个线程处理消息,其中一个线程丢失,那么消息也可能会丢失.
  • rmq便引入了应答机制,每次让消息被处理后,进行一次应答,中间件收到了应答,才将该消息删除,如果其中一个线程故障或丢失,便不可能应答,那么该消息便不会被删除,而是重新入队,分发给其他线程处理.

类别

  • 自动应答:该应答方式是官方默认的应答方式,这种应答方式是以接收到消息后便进行应答,不知道消息还有没有处理完成.所以该应答机制适用于保证线程高效处理消息或允许有少量消息丢失的情况下才可以使用.
  • 手动应答:
  1. basicAck:肯定确认,表明该消息处理完成,可以删除
    2) basicNack:否定确认,表明该消息不处理了(不知道有没有处理),可以删除,可以是否批量处理.
    3) basicReject:和2一样,但不可以批量处理
    4) 批量处理:是否同时处理队列中的多个消息.
    5) 不建议批量处理,希望处理完一个消息便应答一次,不然有些消息还未处理完便被应答.

例子

生产者

private static final String QUEUE="hello2";
public static void main(String[] args) throws Exception{
    Channel channel = RabbitmqUtil.getChannel();
    /*
    * 1.是否持久化
    * 2.是否共享
    * 3.是否自动删除
    * 4.其他消息,死信队列,延迟消息
    * */
    channel.queueDeclare(QUEUE,false,false,false,null);
    Scanner scanner = new Scanner(System.in);
    while (scanner.hasNext()){
        String message = scanner.next();
        channel.basicPublish("",QUEUE,null,message.getBytes());
        System.out.println("消息发送完毕");
    }

}

消费者

    private static final String QUEUE="hello2";

    public static void main(String[] args) throws Exception{
        Channel channel = RabbitmqUtil.getChannel();
        DeliverCallback deliverCallback=(consumerTag,message)->{
            try {
                Thread.sleep(5000);
                System.out.println(new String(message.getBody()));
//            消息处理成功,手动应答,不进行批量处理,
                channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        CancelCallback cancelCallback=(consumerTag)->{
            System.out.println(consumerTag+"->"+"消息被取消");
        };

        channel.basicConsume(QUEUE,false,deliverCallback,cancelCallback);
    }
//和上一个消费者一样只不过让这个消费者沉睡十倍
Thread.sleep(50000);

测试

  • 先启动生产者,因为需要控制台输入,所以产生了一个空队列,但还没有消息.
  • 分别启动两个消费者.
  • 然后给生产者输入消息,发送,确认其中一个消费者收到消息后,立刻再次发送消息,按照轮询方式,第二个消费者此刻会接收到消息,但因为沉睡,时间延迟,在沉睡期间,我们关闭第二个消费者,此刻消息丢失.
  • 按照签收机制,该消息在rmq里会被重新入队,由于此刻只剩下一个消费者,所以该消息会被重新入队让仅剩的一个消费者消费.

持久化

队列持久化

  • 队列持久化为了防止信息丢失.
channel.queueDeclare(QUEUE,false,false,false,null); //第二个参数便表示为是否持久化

消息持久化

  • 将消息标记为持久化并不能保证消息不会丢失,虽然持久化后保存到了磁盘里,但是消息在写入磁盘的瞬间也有可能丢失.
channel.basicPublish("",QUEUE, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
// 第三个参数表示持久化

不公平分发

  • 前面说过消费者通过轮询的方式进行消费,但如果出现某个线程的处理速度缓慢,继续采用轮询的分发消费的方式显然不合理,
  • 所以要采用不公平分发方式,让处理速度多的线程消费多一些消息,能极大地提高系统的效率.
channel.basicQos(1); //在消费者消费消息之前设置,1为不公平分发0为轮询

预取值

  • 前面设置不公平分发的代码也叫设置预取值.
  • rmq本身消息的发送就是异步发送的,所以在任何时候,channel上肯定不止只有一个消息另外来自消费者的手动确认本质上也是异步的。因此这里就存在一个未确认的消息缓冲区,因此希望开发人员能限制此缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题。这个时候就可以通过使用basic.gos.方法设置“预取计数”值来完成的。该值定义通道上允许的未确认消息的最大数量。一旦数量达到配置的数量,RabbitMQ将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认,例如,假设在通道上有未确认的消息5、6、7,8,并且通道的预取计数设置为4,此时RabbitMQ.将不会在该通道上再传递任何消息,除非至少有一个未应答的消息被ack。比方说tag=6这个消息刚刚被确认ACK,RabbitMQ将会感知这个情况到并再发送一条消息。消息应答和QoS预取值对用户吞吐量有重大影响。通常,增加预取将提高向消费者传递消息的速度。虽然自动应答传输消息速率是最佳的,但是,在这种情况下已传递但尚未处理的消息的数量也会增加,从而增加了消费者的RAM消耗(随机存取存储器)应该小心使用具有无限预处理的自动确认模式或手动确认模式,消费者消费了大量的消息如果没有确认的话,会导致消费者连接节点的内存消耗变大,所以找到合适的预取值是一个反复试验的过程,不同的负载该值取值也不同100到300范围内的值通常可提供最佳的吞吐量,并且不会给消费者带来太大的风险。预取值为1是最保守的。当然这将使吞吐量变得很低,特别是消费者连接延迟很严重的情况下,特别是在消费者连接等待时间较长的环境中。对于大多数应用来说,稍微高一点的值将是最佳的。

确认发布

  • 前面说到队列持久化和消息持久化都是将消息保存在磁盘中,但是如果写入磁盘出现问题,该消息还是会丢失.
  • 所以,如果消息中间件进行消息发布并且持久化后,向生产者确认发布,这样才会保证消息的不丢失.

单个确认发布

  • 消息发布一次,中间件必须向生产者确认一次,如果没有确认,第二条消息便不会发出,该确认方式是同步的.
  • 但这种方式效率会很慢.
    	channel.confirmSelect();  //开启确认发布  
		while (scanner.hasNext()){
            String message = scanner.next();
            channel.basicPublish("",QUEUE, null,message.getBytes());
            System.out.println("消息发送完毕");
            channel.waitForConfirms();
        }
// 可以看到,该消息是每次我们发送一条之后,中间件便会将其确认给生产者表示已经发送了,效率会非常缓慢.

批量确认

  • 批量确认是等待消息发布堆积到一定程度后才向生产者确认一次.这样的效率肯定会比的单个确认发布好,但是如果有消息出现问题,难以定位错误消息.
  • 记得开启确认发布

image.png
代码也很简单,只需要在确认发布的时候给一个条件限制便可.

异步确认发布

  • 解决上述两个问题,可以采用异步发布.
  • 解决单个确认的效率问题,我们可以采用批量确认的方式,但是又担心错误消息定位不准确,所以在发送消息的时候我们可以给消息标上序列号,利用key-value的形式存储,key存储序列号,value存储值.这样便可以解决错误消息难以定位的问题.
  • 同时,为了加快生产者发送消息的效率,确认方式采用异步的方式,生产者不用等待中间件反馈,而是让中间件自动反馈(消息发送成功与否),生产者一旦接收到中间件的反馈,立马便会直到该消息是属于发送失败还是成功的,如果失败,再发送一次.
    public static void main(String[] args) throws Exception{
        Channel channel = RabbitmqUtil.getChannel();
        /*
        * 队列名称
        * 1.是否持久化
        * 2.是否共享
        * 3.是否自动删除
        * 4.其他消息,死信队列,延迟消息
        * */
        channel.queueDeclare(QUEUE,false,false,false,null);
        Scanner scanner = new Scanner(System.in);

//        定义一个安全队列,保证消息是否发送成功,成功发送让队列减1,失败仍然保存在安全队列中
        ConcurrentSkipListMap<Long,String> concurrentSkipListMap=new ConcurrentSkipListMap<>();
        /*
        * 1.消息的标记
        * 2.是否为批量消息
        * */
        ConfirmCallback ack=(tag,multiple)->{
            System.out.println("消息发送成功后,安全队列消息减一");
            if (multiple){
//                删除发送成功的消息
                ConcurrentNavigableMap<Long, String> longStringConcurrentNavigableMap =
                        concurrentSkipListMap.headMap(tag);
                longStringConcurrentNavigableMap.clear();
            }else {
                concurrentSkipListMap.remove(tag);
            }
        };
        ConfirmCallback nack=(tag,multiple)->{
            String s = concurrentSkipListMap.get(tag);
            System.out.println("未确认的消息是:"+s);
        };

        //创建消息监听器,在消息发送之前定义
        channel.addConfirmListener(ack,nack);

        while (scanner.hasNext()){
            String message = scanner.next();
            channel.basicPublish("",QUEUE, null,message.getBytes());
//            保存消息到安全队列中
            concurrentSkipListMap.put(channel.getNextPublishSeqNo(),message);

        }
    }
// 注意,监听器必须放在消息发送之前定义,不然有可能消息还未被监听消息已经被消费了.

临时队列

  • 临时队列是非持久化的队列,在服务关闭的时候,该队列会被删除.

image.png
再创建临时队列的时候,直接获取队列,不给创建队列的方法参数值,产生的队列为临时队列,名字是随机的名字.

交换机

  • 在前面的诸多例子中,我们采用的信息消费模式都是一对一模式,也就是说一个消息只能被一个消费者消费,其他消费者不能消费同样的消息.
  • 如果想让多个消费者消费同一个消息,我们可以在队列前面加上一个交换机,通过交换机和队列的绑定,原则还是一对一的消费,只不过这个交换机却可以绑定多个存储相同信息的队列,这样子消费者便可以消费到同样的消息.该模式称为发布订阅模式.

image.png

  • 交换机类型:1.默认类型,是我们在设置交换机为空的时候,系统会给分配默认的交换机,该交换机也称为无名交换机.2.直接交换机3.主题交换机4.标题交换机5.扇出交换机

扇出交换机,路由key相同

生产者

    private static final String QUEUE_NAME="EX1";

    public static void main(String[] args) throws Exception{
        Channel channel = RabbitmqUtil.getChannel();
        //channel.exchangeDeclare(QUEUE_NAME,"fanout");

        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            String message=scanner.next();
            channel.basicPublish(QUEUE_NAME,"",null,message.getBytes());
            System.out.println("消息发送完毕");
        }
    }

消费者

    private static final String EXCHANGE_NAME="EX1";

    public static void main(String[] args) throws Exception{
        Channel channel = RabbitmqUtil.getChannel();
//        创建一个交换机
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
//        创建一个临时队列
        String queue = channel.queueDeclare().getQueue();
//        队列和交换机绑定 1.队列名称,2.交换机,3.路由key
        channel.queueBind(queue,EXCHANGE_NAME,"");

        DeliverCallback deliverCallback=(tag,message)->{
            System.out.println("c1接收的消息->"+new String(message.getBody()));
        };

        CancelCallback cancelCallback=(tag)->{
            System.out.println(tag+"消息发送失败");
        };

        channel.basicConsume(queue,true,deliverCallback,cancelCallback);

    }

注意

  • 交换机写在消费者一方,不能写在生产者一方,不然一个消息还是只能由一个消费者消费.

测试

  • 再复制一份消费者,总共有两份消费者.
  • 先启动两个消费者,然后启动生产者,发送消息,测试成功的结果是:两个消费者都收到了同样的消息.

直接交换机,路由key不同

  • 代码和扇出交换机一样,但是传入路由key时,需要绑定不同的路由key
  • 两者不同的地方在于,直接交换机因为路由key是不同的,可以指定消息发送给某个队列.当然,直接交换机也绑定多个队列,让消息只发送到这几个队列中,只要设置绑定这几个队列时的路由key一样就可以,但是这种情况就有点类似扇出交换机了.
  • 代码类似,但要改变几步
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT); //指定交换机类型
...
channel.queueBind(queue,EXCHANGE_NAME,"v1"); //指定路由key,注意,可以绑定多次,指定不同的路由key,
//但这几个不同的路由key在同一个队列中指定,只能对这个队列生效,其他队列不生效.
  • 测试:启动两个消费者,再启动生产者,在发送消息的时候指定路由key,如果交换机中存在该路由key,那么这个信息便只能发送给这个路由key的队列中.

主题交换机

  • 扇出交换机也是主题交换机的一种,该交换机发布一种消息,不指定路由key,就能让所有的消费者消费同一条消息.
  • 直接交换机也是主题交换机的一种,该交换机指定路由key发送消息,只有存在该路由key的队列才能得到消息,从而被消费者消费.
  • 主题交换机,该交换机的功能囊括了扇出和直接,规定在消费者指定路由key的时候,用多个英文字母(中间用点号隔开)表示路由key,等生产者生产消息的时候便可以指定路由key,如果该路由key符合消费者指定的路由key的时候,这个消费者便可以消费该消息.

image.png

  • 在上图中,Q1队列指定的路由key规则是,三个单词,中间必须是orange,前后各自一个任意单词(*代表一个单词),那么只有生产者发送符合该规则的路由key的消息,Q1才可以接收.Q2队列也规定三个单词,第三个必须是rabbit,第一个和第二个单词必须是一个任意单词,或者,Q2也规定了,多个单词也可以,但是第一个单词必须是lazy(#代表匹配多个单词),
  • 下面给出例子,可以对照上面的图中的规则

image.png

  • 代码几乎一样,但必须修改交换机类型为topic,和路由key

死信队列

  • 该队列主要是为了保存因某些特定原因而导致无法被消费的消息.
  • 存入私信队列中一般因三种情况造成:消息TTL过期(存活时间过期);队列达到最大长度,无法继续存放消息;消息被拒绝(basic.reject或basic.nack),且没有放回原来的队列中.

image.png

消费者

    private static final String normal_queue="normal_queue";
    private static final String dead_queue="dead_queue";
    private static final String normal_ex="normal_ex";
    private static final String dead_ex="dead_ex";

    public static void main(String[] args) throws Exception{
        Channel channel = RabbitmqUtil.getChannel();
        channel.exchangeDeclare(normal_ex, BuiltinExchangeType.DIRECT);
        channel.exchangeDeclare(dead_ex, BuiltinExchangeType.DIRECT);
//        设置参数
        Map<String,Object> map=new HashMap<>();
//        设置过期时间,但是这个通常在生产者一方设置,比较灵活
//        map.put("x-message-ttl",10000);
//        设置正常队列的私信交换机
        map.put("x-dead-letter-exchange",dead_ex);
        map.put("x-dead-letter-routing-key","lisi");

        channel.queueDeclare(normal_queue,false,false,false,map);
        channel.queueDeclare(dead_queue,false,false,false,null);
        channel.queueBind(normal_queue,normal_ex,"zhangsan");
        channel.queueBind(dead_queue,dead_ex,"lisi");

        DeliverCallback deliverCallback=(consumerTag,message)->{
            System.out.println("消息为->"+new String(message.getBody()));
        };

        CancelCallback cancelCallback=(consumerTag)->{

        };

        channel.basicConsume(normal_queue,true,deliverCallback,cancelCallback);
    }

注意,这里map传入参数设置死信队列的路由值和死信队列交换机和死信队列绑定时设置的路由key要一致.这里不能单独写一个,必须写全两个,至于为什么,本人测试过,单独写一个的话,虽然能发送消息,也能消费,但是前端的控制台观察消费情况信息会不全.

生产者

    private static final String normal_ex="normal_ex";

    public static void main(String[] args) throws Exception{
        Channel channel = RabbitmqUtil.getChannel();
        AMQP.BasicProperties
                properties=new AMQP.BasicProperties().builder().expiration("10000").build();
        Scanner scanner=new Scanner(System.in);
        while (scanner.hasNext()){
            String message=scanner.next();
            channel.basicPublish(normal_ex,"zhangsan",properties,message.getBytes());
            System.out.println("消息发送完毕");
        }
    }

结果

  • 测试的时候,先启动一次消费者,创造出交换机先,然后关闭消费者,让消费者下线.
  • 第二步便是启动生产者,连续发送多条消息,这个时候消费者因为下线了无法消费消息,十秒钟后消息便会过期,这个时候可以观察前台变化,刚开始发送消息非死信队列里的消息是在等待消费的,但是因为超过了十秒没有消费,死信队列的一栏便出现了几个未被消费的消息.

image.png
image.png

其他

  • 上面的例子是设置超时时间,也可以设置队列的最大长度.代码几乎一样,但要删除掉设置超时时间的代码,我们在消费者一方去设置队列的最大程度即可
map.put("x-max-length",6);
  • 设置该队列的最大长度后,一旦队列长度达到该限制,剩下的消息再放入队列便会失败,进入死信队列.
  • 其次,设置消息拒绝后,该消息也可以进入死信队列.
        DeliverCallback deliverCallback=(consumerTag,message)->{
            String msg = new String(message.getBody());
            if ("a".equals(msg)){
//                1.信息的标记 2.是否放回队列中
                channel.basicReject(message.getEnvelope().getDeliveryTag(),false);
            }else {
                channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
            }
        };

        CancelCallback cancelCallback=(consumerTag)->{

        };
//			必须设置为手动应答
        channel.basicConsume(normal_queue,false,deliverCallback,cancelCallback);

延迟队列

  • 在上面的例子中,非死信队列和生产者之间的消息是没有延迟的,但因为非死信队列下线了,超过十秒后就转入死信队列中,所以死信队列和生产者之间的消息是延迟消息,延迟十秒.
  • 延迟队列的应用十分广泛,如日常的订单系统,订单超过一定时间未支付便自动取消订单或者超过一定时间未支付便发消息提醒用户订单未支付等,都用到延迟队列.

Springboot整合延迟队列

  • 新建Springboot工程,添加以下依赖
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--RabbitMQ 依赖-->
        <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>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--swagger-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--RabbitMQ 测试依赖-->
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
  • 添加Swagger类
package com.hyb.springbootrabbitmq.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket webApiConfig(){
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("webApi")
                .apiInfo(webApiInfo())
                .select()
                .build();
    }
    private ApiInfo webApiInfo(){
        return new ApiInfoBuilder()
                .title("rabbitmq 接口文档")
                .description("本文档描述了 rabbitmq 微服务接口定义")
                .version("1.0")
                .contact(new Contact("enjoy6288", "http://atguigu.com",
                        "1551388580@qq.com"))
                .build();
    } }
  • 配置properties文件
# 服务器地址
spring.rabbitmq.host=192.168.188.99
spring.rabbitmq.port=5672 
spring.rabbitmq.username=admin
#前台和密码
spring.rabbitmq.password=123 
  • 配置完毕,撰写代码,但在之前,要先看清楚延迟队列的架构图


可以看出,一个消费者可以消费不同延迟时间的消息,只要将路由key映射到不同的消息队列即可.

队列配置类

package com.hyb.springbootrabbitmq.config;



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

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

@PropertySource(value ="classpath:/application.properties")
@Configuration

public class TtlQueueConfig {

//    普通交换机
//    @Value("${ex.normal.ex_1}")
    private static final String normal_ex ="x";

//    死信交换机
//    @Value("${ex.dead.ex_1}")
    private static final   String dead_ex="y";

//    普通队列
//    @Value("${queue.normal.queue_1}")
    private static final String normal_queue_1="x_queue_1";
//    @Value("${queue.normal.queue_2}")
    private static final String normal_queue_2="x_queue_2";

//    死信队列
//    @Value("${queue.dead.queue_1}")
    private static final String dead_queue_1="y_queue_1";

    @Bean("normal_ex")
    public DirectExchange normal_ex(){
        return new DirectExchange(normal_ex);
    }

    @Bean("dead_ex")
    public DirectExchange dead_ex(){
        return new DirectExchange(dead_ex);
    }

//    普通队列
    @Bean("normal_queue_1")
    public Queue normal_queue_1(){
        Map<String,Object> map=new HashMap<>(3);
//        设置死信交换机
        map.put("x-dead-letter-exchange",dead_ex);
//        设置路由key
        map.put("x-dead-letter-routing-key","y_1");
//        设置过期时间
        map.put("x-message-ttl",10000);
        return QueueBuilder.durable(normal_queue_1).withArguments(map).build();
    }


    @Bean("normal_queue_2")
    public Queue normal_queue_2(){
        Map<String,Object> map=new HashMap<>(3);
//        设置死信交换机
        map.put("x-dead-letter-exchange",dead_ex);
//        设置路由key
        map.put("x-dead-letter-routing-key","y_1");
//        设置过期时间
        map.put("x-message-ttl",40000);
//        队列构建类.持久化函数.带着参数.构建
        return QueueBuilder.durable(normal_queue_2).withArguments(map).build();
    }

//      死信队列
    @Bean("dead_queue_1")
    public Queue dead_queue_1(){
        return QueueBuilder.durable(dead_queue_1).build();
    }


//    普通队列和普通交换机的绑定
    @Bean
    public Binding binding_normal_queue_1(@Qualifier("normal_queue_1")Queue normal_queue_1
    , @Qualifier("normal_ex")DirectExchange normal_ex){
        return BindingBuilder.bind(normal_queue_1).to(normal_ex).with("x_1");
    }
    @Bean
    public Binding binding_normal_queue_2(@Qualifier("normal_queue_2")Queue normal_queue_2
    , @Qualifier("normal_ex")DirectExchange normal_ex){
        return BindingBuilder.bind(normal_queue_2).to(normal_ex).with("x_2");
    }

//    死信队列和死信交换机的绑定
    @Bean
    public Binding binding_dead_queue_1(@Qualifier("dead_queue_1")Queue dead_queue_1
    , @Qualifier("dead_ex")DirectExchange dead_ex){
        return BindingBuilder.bind(dead_queue_1).to(dead_ex).with("y_1");
    }
}

注意:这里只是一个配置类,我们将队列和交换机等注册进入,是变成了一个对象的,在其他类中使用该队列的时候要传入队列名称时,不要将对象名称传进入,而是要将原始的队列和交换机名称传进入.

消费者

package com.hyb.springbootrabbitmq.Consumer;

import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.boot.autoconfigure.amqp.RabbitProperties;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;

import java.util.Date;

@Slf4j
@Component
public class MsgConsumer {

//    这里也不能写dead_queue_1,因为这个在IOC里是对象,而你以queues = "dead_queue_1"传入是字符串
    @RabbitListener(queues = "y_queue_1")
    public void receive(Message message, Channel channel){
        log.info("当前时间:{},收到消息为:{}",new Date().toString(),new String(message.getBody()));
    }
}

生产者

package com.hyb.springbootrabbitmq.Consumer;

import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.boot.autoconfigure.amqp.RabbitProperties;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;

import java.util.Date;

@Slf4j
@Component
public class MsgConsumer {

//    这里也不能写dead_queue_1,因为这个在IOC里是对象,而你以queues = "dead_queue_1"传入是字符串
    @RabbitListener(queues = "y_queue_1")
    public void receive(Message message, Channel channel){
        log.info("当前时间:{},收到消息为:{}",new Date().toString(),new String(message.getBody()));
    }
}

测试

  • 这里我们properties文件中配置的Tomcat端口号是8081,所以我们运行启动类的时候,如果没报错,可以先查看前台看队列是否产生.
  • 只要产生了队列,就输入浏览地址:localhost:8081/send/嘻嘻嘻 发送一条消息.
  • 然后等待十秒,看控制台,会发现输出了带有嘻嘻嘻的一则消息

![image.png](https://img-blog.csdnimg.cn/img_convert/b4e622b4bad428011b31bd530a113230.png#clientId=u37e4503a-1125-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=49&id=u6ef77ce1&margin=[object Object]&name=image.png&originHeight=97&originWidth=1149&originalType=binary&ratio=1&rotation=0&showTitle=false&size=24288&status=done&style=none&taskId=u1506c7ee-b704-4adc-a573-111f2ed8e1f&title=&width=574.5)

优化

  • 前面我们是在消费者一方设置过期时间,但在没有整合Springboot之前讲过,若是将过期时间在生产者一方设置会更加灵活,消息队列便可以重复使用.
  • 所以这里我们可以再设置另一个队列,不在消费者一方设置过期时间
package com.hyb.springbootrabbitmq.config;



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

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

@PropertySource(value ="classpath:/application.properties")
@Configuration

public class TtlQueueConfig {

...

//    长期队列
    private static final String long_time_queue="l_queue";

...

//    长期队列
    @Bean("l_queue")
    public Queue l_queue(){
        Map<String,Object> map=new HashMap<>();
        map.put("x-dead-letter-exchange",dead_ex);
        map.put("x-dead-letter-routing-key","y_1");
        return QueueBuilder.durable(long_time_queue).withArguments(map).build();
    }


...

//    长期队列和普通交换机的绑定
    @Bean
    public Binding binding_long_queue(@Qualifier("l_queue")Queue l_queue,
                                      @Qualifier("normal_ex")DirectExchange normal_ex){
        return BindingBuilder.bind(l_queue).to(normal_ex).with("x_3");
    }
}

  • 生产者

    @RequestMapping("/send/{message}/{ttl}")
    public void send(@PathVariable String message,@PathVariable String ttl){
        log.info("当前时间:{},发送一条消息:{}给一个队列,过期时间为:{}",new Date().toString(),message,ttl);
        rabbitTemplate.convertAndSend("x","x_3","消息来自动设置时间的队列:"+message,msg->{
//            设置过期时间
            msg.getMessageProperties().setExpiration(ttl);
            return msg;
        });
    }
  • 测试: localhost:8081/send/message/ttl

image.png
但看结果我们会发现,虽然我们设置了过期时间,但是这个过期时间只针对第一条消息有效,而我们发送了第二条甚至更多条消息,即使过期时间不一样,也不会生效,只会遵循第一条.所以,我们一般不建议在代码层面设置过期时间.

插件

  • rabbitmq 为了解决上述问题,给我们提供了插件机制去进行延迟消息的发送

安装

  • 下载 rabbitmq_delayed_message_exchange-3.8.0.ez
  • 然后将该包转移到/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins 目录下
  • 之后安装插件:rabbitmq-plugins enable rabbitmq_delayed_message_exchange
  • 然后重启/sbin/service rabbitmq-server restart
  • 重启成功后,刷新前台,查看成功结果

成功结果

image.png

延迟图解

  • 在前面的延迟图解中,生产者对于普通队列是没有延迟效果的,只有对于死信队列才有延迟效果.而如果我们假如了延迟插件,延迟消息便会被该插件去处理,这个延迟便给交换机去进行,这个时候,我们的生产者和普通队列就有了延迟的效果,不用再经过死信队列.

配置类

package com.hyb.springbootrabbitmq.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.CustomExchange;
import org.springframework.amqp.core.Queue;
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;

@Configuration
public class DelayedQueueConfig {

//    延迟队列
    private static final String delay_queue="delay_queue";
//    延迟交换机
    private static final String delay_exchange="delay_exchange";
//    routingkey
    private static final String routingkey="rk";

    @Bean("delayExchange")
    public CustomExchange delayExchange(){
        Map<String,Object> map=new HashMap<>();
        map.put("x-delayed-type","direct");
        /*
        * 方法参数
        * 1.交换机名称
        * 2.交换机类型
        * 3.是否持久化
        * 4.是否需要自动删除
        * 5.其他参数
        * */
        return new CustomExchange(delay_exchange,"x-delayed-message",true,false,map);
    }

    @Bean
    public Queue delayedQueue(){
        return new Queue(delay_queue);
    }

    @Bean
    public Binding bindingDelayed(@Qualifier("delayExchange")CustomExchange delay_exchange,
                                  @Qualifier("delayedQueue")Queue delayedQueue){
        return  BindingBuilder.bind(delayedQueue).to(delay_exchange).with(routingkey).noargs();
    }


}

生产者

    @RequestMapping("/send/{message}/{delayed}")
    public void send(@PathVariable String message,@PathVariable Integer delayed){
        log.info("当前时间:{},发送一条消息:{}给一个队列,过期时间为:{}",new Date().toString(),message,delayed);
        rabbitTemplate.convertAndSend("delay_exchange","rk","消息来自动设置时间的队列:"+message, msg->{
//            设置过期时间
            msg.getMessageProperties().setDelay(delayed);
            return msg;
        });
    }

消费者

    @RabbitListener(queues = "delay_queue")
    public void receive1(Message message){
        log.info("当前时间:{},收到消息为:{}",new Date().toString(),new String(message.getBody()));
    }

测试

  • 同样的,发送两条消息

image.png

确认发布

  • 日后,在开发中,虽然会搭建集群,一般不会出错,但总会有偶然时候.
  • 这里我们以一条信息链为例

  • 在此过程中,无论是生产者和交换机或者是交换机和队列之间,任何一条链断开的,都会造成信息丢失.前面讲解发布确认的时候说过,可以同步发布确认的方式防止信息丢失.下面便是整合Springboot的例子

配置文件

  • 在properties配置文件配置
spring.rabbitmq.publisher-confirm-type=correlated
# spring.rabbitmq.publisher-confirm-type 有三种情况
# 1.默认为none,不启动发布确认模式
# 2.simple 发布确认简单模式,发布一条确认一条,效率不高
# 3.correlated 高级模式:异步确认

回调接口

  • 发布确认回调接口为了处理消息发布之后的情形.
package com.hyb.springbootrabbitmq.Consumer;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Slf4j
@Component
public class MyCallback implements RabbitTemplate.ConfirmCallback {

    @Autowired
    RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void  init(){
        rabbitTemplate.setConfirmCallback(this);
    }

    /*
    * 1.返回的消息数据
    * 2.消息是否成功发送
    * 3.消息失败的原因
    * */

    @Override
    public void confirm(CorrelationData correlationData, boolean b, String s) {

        String id=correlationData!=null?correlationData.getId():"";

        if (b){
            log.info(id+"发布成功");
        }
        else {
            log.info(id+"发布失败"+";原因是:"+s);
        }
    }
}

该情形可以处理当信息发布失败或者成功.

生产者

    @RequestMapping("/send/{message}/{delayed}")
    public void send(@PathVariable String message,@PathVariable Integer delayed){
        CorrelationData correlationData=new CorrelationData();
        correlationData.setId(UUID.randomUUID().toString());
        log.info("当前时间:{},发送一条消息:{}给一个队列,过期时间为:{}",new Date().toString(),message,delayed);
        rabbitTemplate.convertAndSend("delay_exchange","rk","消息来自动设置时间的队列:"+message, msg->{
//            设置过期时间
            msg.getMessageProperties().setDelay(delayed);
            return msg;
        },correlationData);
    }

测试

  • 先发送一条消息,可以收到,保证代码没有出错.
  • 然后,交换机名字写错,模拟交换机和生产者之间的链路断开,发送消息,此刻消息是发送失败的,会调用回调函数,输出失败的接口回调.
  • 最后,交换机名称更换回正确的,让交换机和队列的路由key错误,模拟交换机和队列之间的链路出现问题,之后发送消息,会发现,交换机和生产者之间的链路一切正产,但是交换机和队列之间的链路出现了错误,但没有打印错误日志,而是消费者一方接收不到队列出错的那条消息.
  • 所以,我们还需要进一步处理错误队列的那条消息.

回退消息

  • 当交换机和队列之间的链路出现了错误,前面演示的是路由key出现了错误,这个时候,中间件是默认将该消息丢弃的,但此刻生产者是不知道该消息是否被丢弃.所以我们需要将该消息回退.

配置文件

  • properties配置文件配置启动回退接口
spring.rabbitmq.publisher-returns=true

配置类

  • 和回调接口一样,这里放在一起写
package com.hyb.springbootrabbitmq.Consumer;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Slf4j
@Component
public class MyCallback implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {

    @Autowired
    RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void  init(){
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
    }

    /*
    * 1.返回的消息数据
    * 2.消息是否成功发送
    * 3.消息失败的原因
    * */

    @Override
    public void confirm(CorrelationData correlationData, boolean b, String s) {

        String id=correlationData!=null?correlationData.getId():"";

        if (b){
            log.info(id+"发布成功");
        }
        else {
            log.info(id+"发布失败"+";原因是:"+s);
        }
    }

//    只有当消息没有到达目的地的时候,回退函数才会起作用,成功不会启动

    /*
    * 1.回退的消息
    * 2.回退码
    * 3.回退原因
    * 4.回退交换机
    * 5.回退路由key
    * */
    @Override
    public void returnedMessage(Message message, int i, String s, String s1, String s2) {

    }
}

备份交换机

  • 如果不进行消息回退,还可以让消息进入备份交换机

  • 备份交换机的架构和死信队列的架构几乎一样,只不过消息发布失败后,转发到的目的地不同而已.当交换机中有消息发布失败后,该消息会转发到备份交换机上,然后通过备份交换机去转发给消费者,备份交换机的不同在于,其设置了一个警告队列,让消费者清晰明了.
  • 我们在原来的基础上,新建一个交换机和两个队列.然后让他们建立连接.
  • 之后,将原来的交换机和备份交换机建立连接.

image.png

  • 然后新建一个报警消费者,该消费者几乎和原来的消费者是一样,只不过接收的队列是警告队列或者备份队列.

优先级

  • 如果同时写备份交换机和回退机制,以备份交换机为准

优先级队列

  • 队列的数据是先进先出的,如果按照这个规则,消费者消费也是按先进先出的消息进行消费.
  • 但在实际的应用当中,比如大客户和小客户的消息推送,我们肯定是按照大客户先进行推送,但是有可能小客户的消息先进入队列,那么消费者收到的可能是小客户的消息.
  • 为了将消息优先发送给大客户,我们可以给队列中的消息注册一个优先级,优先级越高,消息越排在前面,然后被消费.这个优先级是0-255的.

生产者

Map<String,Object> map=new HashMap<>();
map.put("x-max-priority",10); // 设置最大的优先级为10
channel.queueDeclare(QUEUE,true,false,false,map);
//        消息
for (int i = 0; i < 10; i++) {
    String message=i+"消息";
//            如果i等于5的时候,可以设置该优先级为5,等到接收消息的时候,没有设置优先级的消息将会按照序号排列,这个设置了优先级为5的,将最先出队
    if (i==5){
        AMQP.BasicProperties basicProperties=new AMQP.BasicProperties()
            .builder().priority(5).build();
        channel.basicPublish("",QUEUE,basicProperties,message.getBytes());
    }else {
        channel.basicPublish("",QUEUE,null,message.getBytes());
    }

}

惰性队列

  • 正常情况的队列是保存在内存中,这样读取速度最快.
  • 但惰性队列是保存在磁盘中,消费者如果要消费消息,必须先将磁盘中的信息读取到内存中,才能读取消息,性能比正常队列低.
  • 但如果消费者宕机,消息长时间没有人去消费者,产生消极积压,就适合用惰性队列,先将大量的积压的消息保存在硬盘中,等下次消费者上线再慢慢读取出来.这样就不会长期占用中间件太多的内存.
  • 设置惰性队列很简单,只要在声明队列的时候,map再传入一个参数即可
args.put("x-queue-mode", "lazy");
  • 惰性队列内存消耗比较小,但是占用磁盘空间,而正常队列内存消耗大,不占用磁盘空间

集群搭建

  • 准备三台机器,这里以一台机器克隆出两台,总共为三台的教程
  • 克隆完毕后 ip addr 查看机器的ip地址,然后远程登录这三台
  • 每台机器 vim /etc/hostname 修改对应的hostname名称,比如Hadoop2,Hadoop3,Hadoop4
  • 配置个节点的host文件vim /etc/hosts (注意:建议也修改一下windows系统的hosts文件,内容一样)
192.168.188.100 hadoop2
192.168.188.130 hadoop3
192.168.188.131 hadoop4
  • 在安装有rabbitmq的第一台机器上

scp /var/lib/rabbitmq/.erlang.cookie root@hadoop3:/var/lib/rabbitmq/.erlang.cookie
scp /var/lib/rabbitmq/.erlang.cookie root@hadoop4:/var/lib/rabbitmq/.erlang.cookie
确保另外两台的cookie相同
如果报以下错误

It is also possible that a host key has just been changed.
The fingerprint for the ECDSA key sent by the remote host is
SHA256:Zkr+O0PpjQbv6XAOZwzcuB/fVT0Nz5YFkmB6SNftiyw.
Please contact your system administrator.
Add correct host key in /root/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in /root/.ssh/known_hosts:4
ECDSA host key for hadoop3 has changed and you have requested strict checking.
Host key verification failed.
lost connection
说明之前你和别的机器配置过ssh免密登录,解决办法,查看这句话Offending ECDSA key in /root/.ssh/known_hosts:4 然后vim /root/.ssh/known_host 进入该文件,将你配置的ssh密令删除,只删除需要删除,比如你克隆出的服务器之前就存在过同名的.
  • 安装成功后,启动 RabbitMQ 服务,顺带启动 Erlang 虚拟机和 RbbitMQ 应用服务(在三台节点上分别执行以 下命令 rabbitmq-server -detached
  • 在节点 2 执行

rabbitmqctl stop_app
(rabbitmqctl stop 会将 Erlang 虚拟机关闭,rabbitmqctl stop_app 只关闭 RabbitMQ 服务)
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@hadoop2 // 第二个节点连接到第一个节点
rabbitmqctl start_app(只启动应用服务)

  • 在节点 3 执行

rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster rabbit@hadoop3 //节点三连接到节点二
rabbitmqctl start_app

  • rabbitmqctl cluster_status 查看集群状态
  • 需要重新设置用户 (在任意一个节点上运行以下代码)

创建账号 rabbitmqctl add_user admin 123
设置用户角色 rabbitmqctl set_user_tags admin administrator
设置用户权限 rabbitmqctl set_permissions -p “/” admin “." ".” “.*”

image.png

  • 解除集群节点(node2 和 node3 机器分别执行)

rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
rabbitmqctl cluster_status
rabbitmqctl forget_cluster_node rabbit@hadoop3/hadoop4(第一台机器上执行)

镜像队列

  • 上面虽然搭建了集群,但是如果我们队某个节点消费信息时,该节点宕机,消息还是会丢失,无法在其他节点上获取该队列信息.
  • 这是因为我们没有给队列备份,所谓备份便是镜像队列,给当前队列的集群备份一个队列到其他节点上,一旦该节点宕机了发生了信息丢失,备份的节点便会起效,而且备份的节点也会重新备份出另一个节点,永远保持两份.这样循环下去便保证信息的不丢失.

image.png
image.png
Name 名字随便,而Pattern也随便,不过这表示我们的镜像队列前缀必须为这个.

  • 测试:我们对产生备份队列的原队列启动消费者,为该节点创建一个带有消息的队列,然后可以在前台中查看到队列中会有+1的样式.

image.png
该+1的演示表明,在hadoop4里,有一个镜像队列,点击mirro_hello可进入该队列查看详细信息,然后找到下图

image.png
可以清楚地看到,备份队列和原队列.这样子,只要有一个节点宕机,我们的消费者便可以消费其他节点的信息,保证信息时通用的,不丢失.

Haproxy 实现负载均衡


Federation Exchange(联邦交换机)

  • (broker 北京),(broker 深圳)彼此之间相距甚远,网络延迟是一个不得不面对的问题。有一个在北京

的业务(Client 北京) 需要连接(broker 北京),向其中的交换器 exchangeA 发送消息,此时的网络延迟很小, (Client 北京)可以迅速将消息发送至 exchangeA 中,就算在开启了 publisherconfirm 机制或者事务机制的 情况下,也可以迅速收到确认信息。此时又有个在深圳的业务(Client 深圳)需要向 exchangeA 发送消息, 那么(Client 深圳) (broker 北京)之间有很大的网络延迟,(Client 深圳) 将发送消息至 exchangeA 会经历一 定的延迟,尤其是在开启了 publisherconfirm 机制或者事务机制的情况下,(Client 深圳) 会等待很长的延 迟时间来接收(broker 北京)的确认信息,进而必然造成这条发送线程的性能降低,甚至造成一定程度上的 阻塞。 将业务(Client 深圳)部署到北京的机房可以解决这个问题,但是如果(Client 深圳)调用的另些服务都部 署在深圳,那么又会引发新的时延问题,总不见得将所有业务全部部署在一个机房,那么容灾又何以实现? 这里使用 Federation 插件就可以很好地解决这个问题.

  • 在每台机器上执行以下两步

rabbitmq-plugins enable rabbitmq_federation
rabbitmq-plugins enable rabbitmq_federation_management
成功如图:
image.png

  • 原理图:

image.png
从原理图可以看出,我们只要让上游的交换机同步下游的交换机,就可以实现数据同步,而实现了数据同步,消费者访问北京的机房便可以直到深圳的数据了,但值得注意的是,下游的交换机必须先存在才能被上游同步.

  • .在 downstream(node2)配置 upstream(node1)

image.png

  • 添加 policy

image.png

  • 成功案例

image.png

Federation Queue

  • 联邦队列可以在多个 Broker 节点(或者集群)之间为单个队列提供均衡负载的功能。一个联邦队列可以

连接一个或者多个上游队列(upstream queue),并从这些上游队列中获取消息以满足本地消费者消费消息
的需求.
image.png

  • 添加 upstream(同上)
  • 添加 policy

image.png

Shovel(铲子)

  • Federation 具备的数据转发功能类似,Shovel 够可靠、持续地从一个 Broker 中的队列(作为源端,即

source)拉取数据并转发至另一个 Broker 中的交换器(作为目的端,即 destination)。作为源端的队列和作
为目的端的交换器可以同时位于同一个 Broker,也可以位于不同的 Broker 上。Shovel 可以翻译为"铲子",
是一种比较形象的比喻,这个"铲子"可以将消息从一方"铲子"另一方。Shovel 行为就像优秀的客户端应用
程序能够负责连接源和目的地、负责消息的读写及负责连接失败问题的处理。

  • .开启插件(需要的机器都开启)

rabbitmq-plugins enable rabbitmq_shovel
rabbitmq-plugins enable rabbitmq_shovel_management

  • 原理图(在源头发送的消息直接回进入到目的地队列)

image.png
可以看到,传送过来两条消息,Q1只会有一条,而Q2会有两条,一是自己的消息,二是Q1往自己这边同步的一条消息.

  • 添加 shovel 源和目的地

image.png

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值