Rabbitmq持久化与消息可靠性

一、持久化机制

1.1、RabbitMQ 持久化机制

不管是持久化的消息还是非持久化的消息都可以被写入到磁盘,非持久化的消息在内存不够用时,有一部分数据会持久化到磁盘;重启之后,这些数据是不存在的。
具体持久化流程图如下:
持久化模型图

1.1.1、队列持久化

队列的持久化是在定义队列时的durable参数来实现的,durable为true时,队列才会持久化。

    Connection connection = connectionFactory.newConnection();
    Channel channel = connection.createChannel();
    //第二个参数设置为true,即durable=true
    channel.queueDeclare("queue1", true, false, false, null);

持久化的队列在管理界面可以看到有个“D”的标识

1.1.2、消息持久化

消息持久化通过消息的属性deliveryMode来设置是否持久化,在发送消息时通过basicPublish的参数传入。

   //通过传入MessageProperties.PERSISTENT_TEXT_PLAIN就可以实现消息持久化
    channel.basicPublish("", "queue1", MessageProperties.PERSISTENT_TEXT_PLAIN, "persistent_test_message".getBytes());

1.1.3、交换器持久化

同队列一样,交换器也需要在定义时设置持久化标识,否则在broker重启后将丢失

    //durable为true则开启持久化
    Exchange.DeclareOk exchangeDeclare(String exchange,String type,boolean durable)throws IOException;

1.2、RabbitMQ 内存控制

1.2.1、内存告警

当内存使用超过配置的阈值或者磁盘剩余空间低于配置的阈值时,rabbitmq会暂时阻塞客户端的连接,并停止接收从客户端发来的消息,以此避免服务崩溃,客户端与服务端的心跳检测也会失效。

1.2.2、内存控制

当出现内存告警时,可以通过管理命令临时调整内存大小

    rabbitmqctl set_vm_memory_high_watermark <fraction>

fraction为内存阈值,rabbitmq默认值为0.4,表示当rabbitmq使用的内存超过40%时,就会产生告警并阻塞所有生产者连接。

通过此命令修改的阈值在broker重启后将会失效,通过修改配置文件的方式设置的阈值则不会在重启后消失,但需要重启broker才会生效。

配置文件地址:/etc/rabbitmq/rabbitmq.conf

    vm_memory_high_watermark.relative = 0.4
    #vm_memory_high_watermark.absolute = 1GB

RabbitMQ提供relative或absolute两种配置方式
relative:相对值,即前面的fraction,建议取值在0.4~0.66之间,不建议超过0.7
absolute:单位为KB、MB、GB,对应的命令是:

     rabbitmqctl set_vm_memory_high_watermark absolute <value> 

1.2.3 内存换页

在某个Broker节点触及内存并阻塞生产者之前,它会尝试将队列中的消息换页到磁盘一释放内存空间。

持久化、非持久化的消息都放入磁盘,其中持久化的消息本身就在磁盘中有一个副本这里会将持久化的消息从内存中清除掉。

默认情况下,在内存到达内存阈值的50%时会进行换页动作。
也就是说,在默认的内存阈值为0.4的情况下,当内存超过0.4 * 0.5 =0.2 时,会进行换页动作。

可以通过在配置文件中配置vm_memory_high_watermark_paging_ratio项来修改此值

vm_memory_high_watermark.relative=0.4
vm_memory_high_watermark_paging_ratio=0.75

以上配置将会在rabbitmq内存使用率达到30%时进行换页动作,并在40%时阻塞生产者。
vm_memory_high_watermark_paging_ratio的值大于1时,相当于禁用了换页功能。

1.3、RabbitMQ 磁盘控制

1.3.1 磁盘告警

当磁盘剩余空间低于确定的阈值时,rabbitmq同样会阻塞生产者,这样可以避免因非持久化的消息持续换页而耗尽磁盘空间导致服务崩溃。

默认情况下,磁盘阈值为50MB,表示当磁盘剩余空间低于50MB时,会阻塞生产者并停止内存中消息的换页动作。

这个阈值的设置可以减小,但不能完全消除因磁盘耗尽而导致崩溃的可能性。

例如:在两次磁盘空间检测期间内,磁盘空间从大于50MB被耗尽到0MB。

一个相对谨慎的做法是将磁盘阈值设置为与操作系统所显示的内存大小一致。    

1.3.2 磁盘限制

通过命令可以临时调整磁盘阈值

    rabbitmqctl set_disk_free_limit <disk_limit>
    rabbitmqctl set_disk_free_limit_mem_ralative <fraction>

disk_limit为固定大小,单位为KB、MB、GB
fraction为相对比值,建议的取值为1.0~2.0之间

对应的配置如下:

        disk_free_limit.ralative=2.0
        #disk_free_limit.absolute=50mb

二、消息可靠性和插件化机制

2.1、rabbitmq消息可靠性

一般是业务系统接入消息中间件时首要考虑的问题,一般通过三个方面保障:
发送可靠性:确保消息成功发送到Broker
存储可靠性:Broker对消息持久化,确保消息不会丢失
消费可靠性:确保消息成功被消费

2.1.1、发送可靠性

一般消息发送可靠性分为三个层级:

At most once:最多一次,消息可能会丢失,但绝不会重复传输
At least once:最少一次,消息绝不会丢失,但可能会重复传输
Exactly once:恰好一次,每条消息肯定会被传输一次且仅传输一次

Rabbitmq支持其中的“最多一次”和“最少一次”

其中“最少一次”投递实现需要考虑以下这几个方面的内容:
消息生产者需要开启事务机制或者publisher confirm机制,以确保消息可以可靠地传输到rabbitmq中。
消息生产者需要配合使用mandatory参数或者备份交换器来确保消息能够从交换器路由到队列中,进而能够保存下来而不会被丢弃。
具体代码如下:

package com.study.rabbitmq;

import com.rabbitmq.client.*;

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

// 可靠生产
// https://www.rabbitmq.com/confirms.html
public class Producer {

    public static void main(String[] args) {
        // 1、创建连接工厂
        ConnectionFactory factory = new ConnectionFactory();
        // 2、设置连接属性
        factory.setHost("192.168.100.242");
        factory.setUsername("admin");
        factory.setPassword("admin");

        Connection connection = null;
        Channel channel = null;

        try {
            // 3、从连接工厂获取连接
            connection = factory.newConnection("生产者");
            // 4、从链接中创建通道
            channel = connection.createChannel();

            // 进入confirm模式, 每次发送消息,rabbtiqm处理之后会返回一个对应的回执消息
            AMQP.Confirm.SelectOk selectOk = channel.confirmSelect();
            // 增加监听器
            ArrayList<String> queues = new ArrayList<>();
            channel.addConfirmListener(new ConfirmListener() {
                @Override
                public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                    // deliveryTag 同一个channel中此条消息的编号 。
                    // 业务..
                    System.out.println("受理成功 " + queues.get((int) deliveryTag) + " " + multiple);
                }

                @Override
                public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                    // 失败重发
                    // queues.get((int) deliveryTag)
                    System.out.println("受理失败 " + deliveryTag);
                }
            });
            // 定义fanout类型的交换器
            channel.exchangeDeclare("ps_test", "fanout");

            for (int i = 0; i < 10; i++) {
                // 消息内容
                String message = "Hello Confirm " + i;
                queues.add(message);
                // 发送消息到ps_test交换器上
                AMQP.BasicProperties basicProperties = new AMQP.BasicProperties();
                channel.basicPublish("ps_test", "", basicProperties, message.getBytes());
                System.out.println("消息 " + message + " 已发送!");
            }

            // 等待20秒
            Thread.sleep(20 * 1000L);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 7、关闭通道
            if (channel != null && channel.isOpen()) {
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (TimeoutException e) {
                    e.printStackTrace();
                }
            }

            // 8、关闭连接
            if (connection != null && connection.isOpen()) {
                try {
                    connection.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

// 进入confirm模式, 每次发送消息,rabbtiqm处理之后会返回一个对应的回执消息
AMQP.Confirm.SelectOk selectOk = channel.confirmSelect();

“最多一次”的方式,生产者随意发送,不过这样很难确保消息会成功发送。

2.1.2、消费可靠性

消费者在消费消息的同时,需要将autoAck设置为false,然后通过手动确认的方式去确认已经正确消费的消息,以免在消费端引起不必要的消息丢失。

具体代码如下:

package com.study.rabbitmq;

import com.rabbitmq.client.*;

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

/**
 * 消息确认机制
 */
public class Consumer {

    private static Runnable receive = new Runnable() {
        public void run() {
            // 1、创建连接工厂
            ConnectionFactory factory = new ConnectionFactory();
            // 2、设置连接属性
            factory.setHost("192.168.100.242");
            factory.setUsername("admin");
            factory.setPassword("admin");

            Connection connection = null;
            Channel channel = null;
            final String clientName = Thread.currentThread().getName();

            try {
                // 3、从连接工厂获取连接
                connection = factory.newConnection("消费者");
                // ###死信队列相关:专门用来存储 出错 出异常的数据
                channel = connection.createChannel();
                // 1、 创建一个exchange
                channel.exchangeDeclare("dlq_exchange", "fanout");
                // 2、 创建一个queue,和exchange绑定起来
                channel.queueDeclare("dlq_queue1", false, false, false, null);
                channel.queueBind("dlq_queue1", "dlq_exchange", "");
                // ######死信队列结束


                // 4、从链接中创建通道
                channel = connection.createChannel();
                // 代码定义交换器
                channel.exchangeDeclare("ps_test", "fanout");
                //  还可以定义一个临时队列,连接关闭后会自动删除,此队列是一个排他队列
                String queueName = "queue1";
                // 队列中有死信产生时,消息会转发到交换器 dlq_exchange。
                Map<String, Object> args = new HashMap<String, Object>();
                args.put("x-dead-letter-exchange", "dlq_exchange");
                channel.queueDeclare(queueName, false, false, false, args);
                // 将队列和交换器绑定
                channel.queueBind(queueName, "ps_test", "");

                // 监听队列
                Channel finalChannel = channel;
                channel.basicConsume(queueName, false, "消费者-手动回执",
                        new DefaultConsumer(finalChannel) {
                            @Override
                            public void handleDelivery(String consumerTag,
                                                       Envelope envelope,
                                                       AMQP.BasicProperties properties,
                                                       byte[] body)
                                    throws IOException {
                                try {
                                    System.out.println("收到消息: " + new String(body));
                                    // TODO 业务处理
                                    long deliveryTag = envelope.getDeliveryTag();
                                    // 模拟业务处理耗时
                                    Thread.sleep(1000L);
                                    // 正常消费
                                    // finalChannel.basicAck(deliveryTag, false);
                                    // 异常消费
                                    finalChannel.basicNack(envelope.getDeliveryTag(), false, false);
                                } catch (InterruptedException e) {
                                    // 异常消费, requeue参数 true重发,false不重发(丢弃或者移到DLQ死信队列)
                                    // finalChannel.basicNack(envelope.getDeliveryTag(), false, false);
                                    e.printStackTrace();
                                }
                            }
                        });


                System.out.println(clientName + " 开始接收消息");
                System.in.read();

            } catch (IOException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            } finally {
                // 8、关闭通道
                if (channel != null && channel.isOpen()) {
                    try {
                        channel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (TimeoutException e) {
                        e.printStackTrace();
                    }
                }

                // 9、关闭连接
                if (connection != null && connection.isOpen()) {
                    try {
                        connection.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    };

    public static void main(String[] args) {
        new Thread(receive, "c1").start();
    }

}

2.2、rabbitmq插件机制

2.2.1 插件的核心功能

rabbitmq支持插件,通过插件可以扩展多种核心功能:
支持多种协议
系统状态监控
其它AMQP 0-9-1交换类型
节点联合。

2.2.2 插件的使用

1、通过命令查看rabbitmq内置插件列表:rabbitmq-plugins list
2、通过rabbitmq-plugins命令启用或禁用插件

    启用:rabbitmq-plugins enable plugin-name
    禁用:rabbitmq-plugins disable plugin-name
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值