RabbitMQ可靠消息投递

一、RabbitMQ实战系列一

1.1 消息的可靠投递

1.2 消息的可靠投递2

1.3 消息的可靠投递3

1.2 消息持久化

1.3 消息确认机制之消息的准确发布

1.4 消息确认机制之消息的正确消费

二、RabbitMQ实战系列二

2.1 RabbitMQ系列(一)--消息中间件MQ如何去选择

2.2 RabbitMQ系列(二)--基础组件

2.3 RabbitMQ系列(三)--Java API

2.4 RabbitMQ系列(四)--消息如何保证可靠性传输以及幂等性

2.5 RabbitMQ系列(五)--高级特性

2.6 RabbitMQ系列(六)--面试官问为什么要使用MQ,应该怎么回答

2.7 RabbitMQ系列(七)--批量消息和延时消息

2.8 RabbitMQ系列(八)--顺序消费模式和迅速消息发送模式

三、RabbitMQ保证消息的可靠性传输

3.1 生产者弄丢了数据

⽣产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为⽹络问题啥的,都有可能。

此时可以选择⽤ RabbitMQ 提供的事务功能,就是⽣产者发送数据之前开启 RabbitMQ 事务channel.txSelect ,然后发送消息,

如果消息没有成功被 RabbitMQ 接收到,那么⽣产者会收到异常报错,此时就可以回滚事务channel.txRollback ,然后重试发送消息;

如果收到了消息,那么可以提交事务channel.txCommit 。

// 开启事务
channel.txSelect
try {
    // 这里发送消息
} catch (Exception e) {
    channel.txRollback
    // 这里再次重发这条消息
}
// 提交事务
channel.txCommit

但是问题是,RabbitMQ 事务机制(同步)⼀搞,基本上吞吐量会下来,因为太耗性能。

所以⼀般来说,如果你要确保说写 RabbitMQ 的消息别丢,可以开启 confirm 模式,在⽣产者那⾥设置开启 confirm 模式之后,

你每次写的消息都会分配⼀个唯⼀的 id,然后如果写⼊了 RabbitMQ 中,RabbitMQ 会给你回传⼀个 ack 消息,告诉你说这个消息 ok 了。

如果RabbitMQ 没能处理这个消息,会回调你的⼀个 nack 接⼝,告诉你这个消息接收失败,你可以重试。

⽽且你可以结合这个机制⾃⼰在内存⾥维护每个消息 id 的状态,如果超过⼀定时间还没接收到这个消息的回调,那么你可以重发。

事务机制和 confirm 机制最⼤的不同在于,事务机制是同步的,你提交⼀个事务之后会阻塞在那⼉,但是 confirm 机制是异步的,

你发送个消息之后就可以发送下⼀个消息,然后那个消息 RabbitMQ 接收了之后会异步回调你的⼀个接⼝通知你这个消息接收到了。

所以⼀般在⽣产者这块避免数据丢失,都是⽤ confirm 机制的。

3.1.1 RabbiMQ消息确认机制-可靠抵达

 1.保证消息不丢失,可靠抵达,可以使用事务消息,性能下降250倍,为此引入确认机制。

2.Publisher ConfimCallback:确认模式

3.Publisher ReturnCallback:未投递到Queue,退回模式

4.Consumer Ack:Ack机制

 

3.1.2 可靠抵达-ConfirmCallback

spring.rabbitmq.publisher-confirms=true

1.在创建 connectionFactory 的时候设置 PublisherConfirms(true) 选项,开启confirmcallback 。

2.CorrelationData:用来表示当前消息唯一性。

3.消息只要被 broker 接收到就会执行 confirmCallback,如果是 cluster 模式,需要所有broker 接收到才会调用 confirmCallback。

4.被 broker 接收到只能表示 message 已经到达服务器,并不能保证消息一定会被投递到目标 queue 里。所以需要用到接下来的 returnCallback 。

3.1.2 可靠抵达-ReturnCallback

spring.rabbitmq.publisher-returns=true

spring.rabbitmq.template.mandatory=true

1.confrim 模式只能保证消息到达 broker,不能保证消息准确投递到目标 queue 里。在有些业务场景下,我们需要保证消息一定要投递到目标 queue 里,此时就需要用到

return 退回模式。

2.这样如果未能投递到目标 queue 里将调用 returnCallback ,可以记录下详细到投递数据,定期的巡检或者自动纠错都需要这些数据。

3.2 RabbitMQ弄丢了数据

就是 RabbitMQ ⾃⼰弄丢了数据,这个你必须开启 RabbitMQ 的持久化,就是消息写⼊之后会持久化到磁盘,哪怕是 RabbitMQ ⾃⼰挂了,

恢复之后会⾃动读取之前存储的数据,⼀般数据不会丢。除⾮极其罕⻅的是,RabbitMQ 还没持久化,⾃⼰就挂了,可能导致少量数据丢失,

但是这个概率较⼩。设置持久化有两个步骤:

  • 创建 queue 的时候将其设置为持久化。这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue ⾥的数据的。
  • 第⼆个是发送消息的时候将消息的 deliveryMode 设置为 2,就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。

必须要同时设置这两个持久化才⾏,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复queue,恢复这个 queue ⾥的数据。

注意,哪怕是你给 RabbitMQ 开启了持久化机制,也有⼀种可能,就是这个消息写到了RabbitMQ 中,但是还没来得及持久化到磁盘上,

结果不巧,此时 RabbitMQ 挂了,就会导致内存⾥的⼀点点数据丢失。所以,持久化可以跟⽣产者那边的 confirm 机制配合起来,

只有消息被持久化到磁盘之后,才会通知⽣产者 ack 了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,⽣产者收不到 ack ,

你也是可以⾃⼰重发的。

3.3 消费端弄丢了数据

RabbitMQ 如果丢失了数据,主要是因为你消费的时候,刚消费到,还没处理,结果进程挂了,⽐如重启了,那么就尴尬了,

RabbitMQ 认为你都消费了,这数据就丢了。这个时候得⽤ RabbitMQ 提供的 ack 机制,简单来说,就是你必须关闭 RabbitMQ 的⾃动ack ,

可以通过⼀个 api 来调⽤就⾏,然后每次你⾃⼰代码⾥确保处理完的时候,再在程序⾥ack ⼀把。这样的话,如果你还没处理完,

不就没有 ack 了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。

消费者获取到消息,成功处理,可以回复Ack给Broker

1.basic.ack用于肯定确认;broker将移除此消息

2.basic.nack用于否定确认;可以指定broker是否丢弃此消息,可以批量

3.basic.reject用于否定确认;同上,但不能批量

默认自动ack,消息被消费者收到,就会从broker的queue中移除

queue无消费者,消息依然会被存储,直到消费者消费

1.消费者收到消息,默认会自动ack。但是如果无法确定此消息是否被处理完成,或者成功处理。我们可以开启手动ack模式

2.消息处理成功,ack(),接受下一个消息,此消息broker就会移除

3.消息处理失败,nack()/reject(),重新发送给其他人进行处理,或者容错处理后ack,消息一直没有调用ack/nack方法,broker认为此消息正在被处理,不会投递给别人,此时客户端断开,消息不会被broker移除,会投递给别人

3.4 案例Demo

  rabbitmq:
    host: 192.168.56.10
    port: 5672
    virtual-host: /
    # 开启发送端确认
    publisher-confirms: true
    # 开启发送端消息抵达队列的确认
    publisher-returns: true
    # 只要抵达队列,以异步方式优先回调我们这个returnconfirm
    template:
      mandatory: true
    # 手动ack消息
    listener:
      simple:
        acknowledge-mode: manual
@Configuration
public class MyRabbitConfig {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 使用JSON序列化机制,进行消息转换
     * @return
     */
    @Bean
    public Jackson2JsonMessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }

    /**
     * 定制RabbitTemplate
     * 1.服务器Broker收到消息就回调
     * 1.1spring.rabbitmq.publisher-confirms=true
     * 1.2 设置确认回调
     *
     * 2.消息正确抵达队列进行回调
     * 2.1 spring.rabbitmq.publisher-returns=true
     *     spring.rabbitmq.template.mandatory=true
     * 2.2 设置确认回调ReturnCallback
     *
     * 3.消费端确认(保证每个消息被正确消费,此时才可以broker删除这个消息)
     * spring.rabbitmq.listener.simple.acknowledge-mode=manual 手动签收
     * 3.1 默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息
     * 问题: 我们收到很多消息,自动回复给服务器ack,只有一个消息处理成功,宕机了。发生消息丢失
     * 消费者手动确认模式: 只要我们没有明确告诉MQ,货物被签收,没有Ack,消息就一直是unacked状态。
     * 即使Consumer宕机。消息不会丢失,会重新变为Ready,下一次有新的Consumer连接进来就发给他
     * 3.2 如何签收
     * channel.basicAck(deliveryTag, false); 签收;业务成功完成就应该签收
     * channel.basicNack(deliveryTag, false);拒签;业务失败,拒签
     */
    @PostConstruct // MyRabbitConfig对象创建完成以后,执行这个方法
    public void initRabbitTemplate() {
        //设置确认回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             * 1.只要消息抵达Broker就ack=true
             * @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id)
             * @param ack 消息是否成功收到
             * @param cause 失败的原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                /**
                 * 1.做好消息确认机制(publisher, consumer[手动ACK])两端确认
                 * 2.每一个发送的消息都在数据库做好记录。定期将失败的消息再次发送一遍
                 */
                //服务器收到了
                //修改消息的状态
                System.out.println("confirm...correlationData[" + correlationData + "]==>ack[" + ack + "]==>cause[" + cause + "]");
            }
        });

        //设置消息抵达队列的确认回调
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {

            /**
             * 只要消息没有投递给指定的队列,就触发这个失败回调
             *
             * @param message 投递失败的消息详细信息
             * @param replyCode 回复的状态码
             * @param replyText 回复的文本内容
             * @param exchange  当时这个消息发给哪个交换机
             * @param routingKey 当时这个消息用哪个路由键
             */
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                //报错误了。修改数据库当前消息的状态->错误,相当于没收到这条消息,后期再进行重发
                System.out.println("Fail Message[" + message + "]==>replyCode" + replyCode + "]==>replyText[" + replyText + "]==>exchange[" + exchange + "]==>routingKey[" + routingKey + "]");
            }
        });
    }
}
@RestController
public class RabbitController {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @GetMapping("sendMq")
    public String sendMq(@RequestParam(value = "num", defaultValue = "10") Integer num) {
        for (int i = 0; i < num; i++) {
            if (i % 2 == 0) {
                OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
                reasonEntity.setName("哈哈-" + i);
                rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", reasonEntity, new CorrelationData(UUID.randomUUID().toString()));
            } else {
                OrderEntity orderEntity = new OrderEntity();
                orderEntity.setOrderSn(UUID.randomUUID().toString());
                //路由键不存在,模拟投递给队列失败,new CorrelationData(UUID.randomUUID().toString())消息的唯一id
                rabbitTemplate.convertAndSend("hello-java-exchange", "hello1.java", orderEntity, new CorrelationData(UUID.randomUUID().toString()));
            }
        }
        return "ok";
    }
}

@RabbitListener(queues = {"hello-java-queue"})
@Service("orderItemService")
public class OrderItemServiceImpl extends ServiceImpl<OrderItemDao, OrderItemEntity> implements OrderItemService {

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<OrderItemEntity> page = this.page(
                new Query<OrderItemEntity>().getPage(params),
                new QueryWrapper<OrderItemEntity>()
        );

        return new PageUtils(page);
    }

    /**
     * queues: 声明需要监听的所有队列
     * org.springframework.amqp.core.Message
     * 参数 可以写一下类型
     * 1.Message message: 原生消息详细信息。头+体
     * 2.T<发送的消息的类型> OrderReturnApplyEntity content
     * 3.Channel channel: 当前传输数据的通道
     * <p>
     * Queue: 可以很多人都来监听。只要收到消息,队列删除消息,而且只能有一个收到此消息
     * 场景:
     * 1.订单服务启动多个,都有此代码,是只有一个服务收到消息,还是都收到消息
     * 同一个消息,只能有一个客户端收到
     * 2.只有一个消息完全处理完,方法运行结束,我们才可以接收到下一个消息
     *
     * @param message
     */
    //@RabbitListener(queues = {"hello-java-queue"})
    @RabbitHandler
    public void receiveMessage(Message message, OrderReturnReasonEntity content, Channel channel) throws InterruptedException {
        //Body:'{"id":1,"orderId":null,"skuId":null,"orderSn":null,"createTime":1617012055776}
        System.out.println("接收到消息...内容: " + message + "--->内容: " + content);
        byte[] body = message.getBody();
        //消息头属性信息
        MessageProperties properties = message.getMessageProperties();
        //System.out.println("接收到消息...内容: " + message + "--->类型: " + message.getClass());
        System.out.println("消息处理完成--> " + content.getName());
        //channel内按顺序自增的
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        System.out.println("deliveryTag==>" + deliveryTag);
        //签收货物,非批量模式multiple:是否批量确认,false做一个签收一个
        try {
            if (deliveryTag % 2 == 0) {
                channel.basicAck(deliveryTag, false);
                System.out.println("签收了货物..." + deliveryTag);
            } else {
                //退货 requeue=false  requeue=true 发回服务器,服务器重新入队
                //long deliveryTag: 拒收哪一个, boolean multiple: 是否批量拒绝, boolean requeue: 拒收以后要不要重新发回mq
                channel.basicNack(deliveryTag, false, true);
                //long deliveryTag, boolean requeue
                //channel.basicReject();
                System.out.println("没有签收货物..." + deliveryTag);
            }
        } catch (IOException e) {
            //网络中断
            e.printStackTrace();
        }
    }

    @RabbitHandler
    public void receiveMessage1(Message message, OrderEntity content, Channel channel) throws InterruptedException {
        //Body:'{"id":1,"orderId":null,"skuId":null,"orderSn":null,"createTime":1617012055776}
        System.out.println("接收到消息...内容: " + message + "--->内容: " + content.getOrderSn());
    }
}

视频教程视频教程视频教程视频教程Rabbit笔记

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值