RabbitMQ消息持久化的保证

      作为消息,我们肯定是不希望在某个过程中它被丢掉找不到了,因此我们应该如何保证消息在传递过程中不会丢失也就成了我们需要解决的问题,既然要保证传递过程数据不丢失,那么我们就需要知道传递的过程有哪些,我们首先来看下消息从生产者到消费者的流转过程,如下面2张图

        从上图可以看出生产者产生消息后,经过序列化后通过connection创建的channel将消息发送到broker节点上面,broker节点里面定义了exchange,routingkey,queue等消息相关组件,消息在被消费者消费之前都是存放到了queue当中,当消息被queue存放好以后,消费者就会通过connection创建的channel将消息获取到,在rabbitmq中有push和pull两种获取消息的方式,消费者获取到消息过后通过反序列化获取到正确的消息,最后进行相应的业务处理。也因此消息如果需要被不丢失,也就需要在上面过程中的传递过程中进行持久化的保存,下面我们就来具体看看在哪些地方可以进行持久化:

      1.生产者产生消息

      生产者产生消息后在发送到broker节点前,为了保证在发送过程中不会因为网路的原因导致数据丢失,我们需要首先在本地进行一次保存,这种保存方式可以是存放到redis、数据库等地方,本地保存完成后才进行发送到broker节点上。

      2.exchange交换器持久化

       消息发送到broker节点的时候,它首先是发送到exchange的,因此消息也就是说需要存放到exchange,然后经过路由发送到queue队列中,因此这个时候我们的exchange就需要一直存在了,如果exchange在broker节点宕机重启后都不存在了,那么存储在它上面的消息当然也不会存在了,因此我们需要将exchange进行持久化保存,其定义如下:

       原生Java定义:

/**
 *
 * @param exchange 交换器名称
 * @param type 交换器类型
 * @param durable 交换器是否持久化,避免重启后,要再次创建。和消息的持久化没关系。
 * @param autoDelete 当没有队列绑定到它时是否自动删除
 * @param internal 是否是MQ内部使用的,如果为true我们就不能在客户端中使用
 * @param arguments
 * @return
 * @throws IOException
 */
public AMQP.Exchange.DeclareOk exchangeDeclare(String exchange, String type, boolean durable, boolean autoDelete,boolean internal, Map<String, Object> arguments) throws IOException {
    return this.exchangeDeclare(exchange, type, durable, autoDelete, durable, arguments);
}
// 创建交换器
channel.exchangeDeclare(EXCHANGE_NAME, "fanout",true,false,false,null);

       springboot中的定义,在springboot中默认是将exchange进行了持久化的,因此直接下面这样定义就可以了

@Bean(name = "ordersExchange")
public TopicExchange ordersExchange() {
    return new TopicExchange("ordersExchange");
}

       3.queue队列持久化

       同exchange交换器一样,我们也需要在broker节点宕机重启后queue队列仍然存在,这样保存在其上的未消费消息才能在重启可以依然在queue中。

      原生Java定义:

/**
 * 
 * @param queue 队列名称,最长255字节,UTF-8字
符。如想要Broker为我们生成队列名,可以在声明创建Queue时传入空字符"",在返回值中可以
取得生成的队列名
 * @param durable 是否持久存储
 * @param exclusive 是否独占,设置为true时被一个connection独占使用,当connection 关闭时Queue也被删除
 * @param autoDelete 是否在Queue的最后一个消费者关闭时自动删除Queue
 * @param arguments  可选的被插件和Broker特殊特性使用的参数,如message TTL, queue length limit
等
 * @return
 */
public com.rabbitmq.client.AMQP.Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments){
......
}

       springboot中的定义:

@Bean(name = "ordersQueue")
public Queue ordersQueue() {
    // 第二个参数表示是否持久化
    return new Queue(RabbitQueueConstant.ORDER_QUEUE, true);
}

// 这个是完整的定义

/**
 * 
 * @param queue 队列名称,最长255字节,UTF-8字
符。如想要Broker为我们生成队列名,可以在声明创建Queue时传入空字符"",在返回值中可以
取得生成的队列名
 * @param durable 是否持久存储
 * @param exclusive 是否独占,设置为true时被一个connection独占使用,当connection 关闭时Queue也被删除
 * @param autoDelete 是否在Queue的最后一个消费者关闭时自动删除Queue
 * @param arguments  可选的被插件和Broker特殊特性使用的参数,如message TTL, queue length limit
等
 * @return
 */
public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, @Nullable Map<String, Object> arguments) {
......
}

       4.消费者消费消息

       消费者在正确消费完消息后就可以告诉broker节点可以删除掉节点中的消息了。

       那么有了以上持久化的方式并不一定能保证消息被正确存储,我们还需要一些确认机制才行。

       1.生产者发送消息到broker节点消息确认

       生产者产生消息后,把消息发送到broker节点后,我们要明确的知道broker节点是否已经成功接收到了消息,只有知道了消息被broker节点接收完成后,我们才可以将保存在本地的消息进行删除,否则就需要进行相关的判断来进行消息的重发以及发送异常消息到相关人员确认消息是什么原因不能正确的发送到相关节点。它有3种确认方式:

      1.1.异步流式确认 事件驱动 开销低,吞吐量大

      原生Java代码:

// 1 开启发布确认模式 就不能再做事务管理了
channel.confirmSelect();
// 2 待确认消息的Map
Map<Long, String> messagesMap = new ConcurrentHashMap<>();

// 3 指定流式确认事件回调处理
channel.addConfirmListener((deliveryTag, multiple) -> { // multiple表示是否是多条的确认
    System.out.println("收到OK ack:deliveryTag=" + deliveryTag + " multiple=" + multiple + ",从Map中移除消息");
// 从Map中移除对应的消息
    messagesMap.remove(deliveryTag);
			}, (deliveryTag, multiple) -> {
    System.out.println("收到 NON OK ack:deliveryTag=" + deliveryTag + " multiple=" + multiple + " 从Map中移除消息,重发或做其他处理");
    // 从Map中移除对应的消息
    String message = messagesMap.remove(deliveryTag);   
    // 重发,或做其他处理
    System.out.println("失败消息:" + message);
});

for (int i = 1; i < 20; i++) {
    // 消息内容
    String message = "消息" + i;
    // 4 将消息放入到map中
    messagesMap.put(channel.getNextPublishSeqNo(), message);
    // 5、发送消息
    channel.basicPublish("mandatory-ex", "", true, null, message.getBytes());
    System.out.println("发布消息:" + message);

    Thread.sleep(2000L);
}

       1.2.批量发布确认 批次等待,确认不ok 一批重发

       原生Java代码:

// 1 开启发布确认模式
channel.confirmSelect();

// 2 发送一批消息
for (int i = 1; i < 10; i++) {
    // 消息内容
    String message = "消息" + i;
    // 发送消息
    channel.basicPublish("mandatory-ex", "", false, null, message.getBytes());
    System.out.println("发布消息:" + message);
}

// 3 等待该批消息的确认结果
boolean batchConfirmResult = channel.waitForConfirms();
// 等待一定时间获取确认结果
// boolean batchConfirmResult = channel.waitForConfirms(3000L);
// 下面两个方法则是以抛出IOException表示失败(任意一个消息 nack 则抛出IOException)
// channel.waitForConfirmsOrDie();
// channel.waitForConfirmsOrDie(timeout);

// 4得到确认结果后的后处理
if (batchConfirmResult) {
    System.out.println("该批确认OK,继续进行下一批发布");
} else {
    System.out.println("该批确认 NON OK,重发该批或做其他处理");
}

       1.3.单条确认 发一条就等待确认

       原生Java代码:

// 生成一个消息返回确认的标志
final String corrId = UUID.randomUUID().toString();

String replyQueueName = channel.queueDeclare().getQueue();
AMQP.BasicProperties props = new AMQP.BasicProperties
                .Builder()
                // 消息标志赋值
                .correlationId(corrId)
                // 用于broker节点返回消息给生产端的queue定义
                .replyTo(replyQueueName)
                .build();
// 发布消息        
channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));

// 消费返回队列中的消息
String ctag = channel.basicConsume(replyQueueName, true, (consumerTag, delivery) -> {
        // 用于判断消息标志是否就是当前消息
        if (delivery.getProperties().getCorrelationId().equals(corrId)) {
            // 进行相关的业务操作
        }
    }, consumerTag -> {
});

       springboot因为经过处理,因此都是如下代码:

       配置文件:

spring:
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest
    #开启发送消息确认
    publisher-confirms: true
    #开启消息回退
    publisher-returns: true
    virtual-host: /
    listener:
      simple:
        #消费者端手动确认消息
        acknowledge-mode: manual

      生产者端代码: 

public class OrdersController implements ConfirmCallback {

    @Resource
    private RabbitTemplate rabbitTemplate;

    @Resource
    private RedisTemplate redisTemplate;

    public void addOrders() {
        // 用于保证传送到rabbit mq后没有正确保存时的回调执行判断
       // 生成uuid主键
       String uuid = UUIDUtil.getUUID();

       //redis中缓存订单信息,防止MQ发送失败
       redisTemplate.opsForHash().put("orderHash", uuid, ordersDTO);
    }

    /**
    * 生产端消息发送到broker后的确认
    */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        // 如果消息投递失败,如何处理
        if (ack) {
            // 删除redis缓存
            redisTemplate.opsForHash().delete("orderHash", correlationData.getId());
        } else {
            String id = correlationData.getId();
            log.error("uuid为{}的订单发送MQ失败:{}", id, cause);

            // 重新投递
            Object ordersDTO = redisTemplate.opsForHash().get("orderHash", id);
            if (Optional.ofNullable(ordersDTO).isPresent()) {
                rabbitTemplate.convertAndSend("ordersExchange",
                        "topic.order",
                        (OrdersDTO) ordersDTO,
                        correlationData);
            }
        }
    }
}

       还有一种情况是消息不能到达broker路由节点,造成这种的原因可能是:交换没有绑定队列以及交换没法根据消息的路由key把消息路由到队列,那么在默认情况下就会把这种消息丢弃,但是现实情况肯定是不能这样的,我们都得保证消息的可靠传输,因此我们针对这种消息的时候就需要采用回退或者死信队列(备用交换)来进行二次传输了。

      退回原生Java代码:

void basicPublish(String exchange, String routingKey, boolean mandatory, BasicProperties props,
byte[] body)
mandatory:true 强制退回, false 不需退回,直接丢弃。

       springboot代码,需要开启退回功能,配置在上面:

@Override
public void returnedMessage(org.springframework.amqp.core.Message message, int replyCode, String replyText, String exchange, String routingKey) {
    // 业务处理
}

       死信队列:

       控制台方式:

#aa表示是哪一个mini rabbitmq节点
rabbitmqctl set_policy aa "^my-direct$" '{"alternate-exchange":"my-ae"}'

       原生Java代码方式:

//声明参数 
Map<String, Object> args = new HashMap<String, Object>(); 
args.put("alternate-exchange", "my-ae"); 
//备用交换参数指定 
channel.exchangeDeclare("my-direct", "direct", false, false, args);
channel.exchangeDeclare("my-ae", "fanout"); 
channel.queueDeclare("routed"); 
channel.queueBind("routed", "my-direct", "key1"); 
channel.queueDeclare("unrouted"); 
channel.queueBind("unrouted", "my-ae", "");

       springboot代码,其他的队列绑定就不在写出:

@Bean(name = "ordersExchange")
public TopicExchange ordersExchange() {
    Map<String, Object> args = new HashMap();
    // 备用交换设置
    args.put("alternate-exchange", "ordersExchangeAlternate");
    return new TopicExchange(RabbitExchangeConstant.ORDER_EXCHANGE, true, false, args);
}

       2.消费者消息确认

       消费者在获取消息的时候有push和pull两种方式,因此在消息确认上也是不一样的,针对不同的消息,我们可以根据优先级来进行消息的处理,设置如下:

      原生Java代码:

Map<String, Object> args = new HashMap<String, Object>(); 
 // 整数,数值越大优先级越高。 默认 0
args.put("x-priority", 10);
channel.basicConsume("my-queue", false, args, consumer);

      springboot代码:

@RabbitListener(queues = "ordersQueue",priority = "10")

      在push模式下有2种确认方式:分别是Automatic自动和Manual手动,自动的情况下它不需要人为去处理,只要消息发送到了消费者就会把broker节点上的消息删除掉,这种方式就会导致也许消费者异常了而没有正确消费到消息,也就导致了消息的丢失,因此在重要消息的情况下一般都采取了Manual手动的模式进行消息的消费,手动确认有3种确认方式:

       A. basic.ack 用于正面确认,消费者确认消息被妥善处理,broker可以移除该消息了。

       B. basic.nack 用于负面确认,扩展了basic.reject,以支持批量确认。是RabbitMQAMQP-0-9-1的 扩展。

       C. basic.reject 用于负面确认,用于单条消息的确认。

       原生Java代码:

// 1、定义收到消息后的回调
DeliverCallback callback = (consumerTag, message) -> {
    System.out.println(consumerTag + " 收到消息:" + new String(message.getBody(), "UTF-8"));

    // 进行消息处理,然后根据处理结果决定该如何确认消息。

    // 获得消息传递标识
    long deliveryTag = message.getEnvelope().getDeliveryTag();
    // 是否批量
    boolean multiple = false; 
    // 是否重配送
    boolean requeue = true; 
    // 手动单条确认消息ok
    // channel.basicAck(deliveryTag, false);
    // 手动单条确认消息reject,并重配送
    channel.basicReject(deliveryTag, requeue);
    // 手动单条确认消息reject,移除不重配送
    // channel.basicReject(deliveryTag, false);
    // 手动单条确认消息Nack,并重配送
    // channel.basicNack(deliveryTag, multiple, requeue);
};

// 设置一定的预取数量,当未确认数达到这个值时,broker将暂停配送消息给此消费者
channel.basicQos(3);

// 2、注册手动确认消费者
channel.basicConsume(queueName, false, callback, consumerTag -> {
});

       springboot代码:

@RabbitListener(queues = "hello")
public void receive(String in, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag)
			throws Exception {
    System.out.println(" [c] Received '" + in + "'");
    // 是否批量
    boolean multiple = false;
     // 是否重配送
    boolean requeue = true;
    // 手动单条确认消息ok
    channel.basicAck(deliveryTag, false);
    // 手动单条确认消息reject,并重配送
    // channel.basicReject(deliveryTag, requeue);
    // 手动单条确认消息reject,移除不重配送
    // channel.basicReject(deliveryTag, false);
    // 手动单条确认消息Nack,并重配送
    // channel.basicNack(deliveryTag, multiple, requeue);
	}

        在pull拉取模式下,每次拉取完成就需要手动进行确认了,如下:

       原生Java代码如下:

// 批量Nack,并重发 
GetResponse gr1 = channel.basicGet("some.queue", false);
GetResponse gr2 = channel.basicGet("some.queue", false);
// 第二个参数 true表示批量
channel.basicNack(gr2.getEnvelope().getDeliveryTag(), true, true); 

      在springboot中需要重新execute()方法:

this.template.execute(channel -> {
    // 从指定队列拉取一条消息
    GetResponse gr = channel.basicGet("queue1", false);

    if (gr == null) {
        // 未取到消息
        System.out.println("队列上没有消息");
    } else { // 取到消息
        // 进行消息处理,然后根据处理结果决定该如何确认消息。
        System.out.println("取得消息:" + new String(gr.getBody(), "UTF-8"));
        System.out.println("消息的属性有:" + gr.getProps());
        System.out.println("队列上的消息数量有:" + gr.getMessageCount());

        // 获得消息传递标识
        long deliveryTag = gr.getEnvelope().getDeliveryTag();
        // 是否批量
        boolean multiple = false;
        // 是否重配送
        boolean requeue = true;
        // 手动单条确认消息ok
        // channel.basicAck(deliveryTag, false);
        // 手动单条确认消息reject,并重配送
        channel.basicReject(deliveryTag, requeue);
        // 手动单条确认消息reject,移除不重配送
        // channel.basicReject(deliveryTag, false);
        // 手动单条确认消息Nack,并重配送
        // channel.basicNack(deliveryTag, multiple, requeue);
    }
    return null;
});

      在一些代码上是参考的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值