RabbitMQ高级特性

消息的可靠投递

在使用Rabbitmq的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景.Rabbitmq为我们提供了两种方式用来控制消息的投递可靠性模式

  • confirm确认模式
  • return模式

rabbitmq整个消息投递的路径为:

provider --> rabbitmq broker --> exchange --> queue --> consumer

  • 消息从provider到exchange则会返回一个confireCallback函数
  • 消息从exchange到queue投递失败则会返回一个returnCallback

通过这两个callback控制消息的可靠性投递

confirm确认模式测试

# 在springboot中有三种模式

- NONE值是禁用发布确认模式,是默认值

- CORRELATED值是发布消息成功到交换器后会触发回调方法

- SIMPLE值经测试有两种效果,
              其一效果和CORRELATED值一样会触发回调方法,
              其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,
              要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker;
# 在配置文件中开启确认模式
spring.rabbitmq.publisher-confirm-type=correlated

测试代码如下:

/**
 * 确认模式
 *  步骤:
 *      1. 在配置文件中开启spring.rabbitmq.publisher-confirm-type=correlated
 *      2. 在rabbitTemplate定义ConfirmCallBack函数
 */
@Test
public void testRoute(){

    rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
        /**
         *
         * @param correlationData  相关配置信息
         * @param ack  exchange交换机是否成功收到了消息  true-成功  false-失败
         * @param cause  失败原因
         */
        @Override
        public void confirm(CorrelationData correlationData, boolean ack, String cause) {
            System.out.println("confirm方法被执行了。。");
            if(ack){
                System.out.println("接收成功消息" + cause);
            } else{
                System.out.println("接受失败" + cause);
                //做一些处理,让消息再次发送
            }
        }
    });
    rabbitTemplate.convertAndSend("directs","info","发送info的key的路由消息");
}

结果如下:

return模式测试

# 在配置文件中开启确认模式
spring.rabbitmq.publisher-returns=true

测试代码如下:

/**
 * 回退模式: 当消息发送给exchange后,exchange路由到queue失败时才会执行  ReturnCallBack
 *   步骤:
 *       1. 开启回退模式
 *       2. 设置ReturnCallBack
 *       3. 设置exchange处理消息的模式:
 *          1)如果消息没有路由到Queue,则丢弃消息(默认)
 *          2)如果消息没有路由到Queue,返回给消息发送方ReturnCallBack
 */
@Test
public void testReturn(){

    //设置交换机处理失败消息的模式
    rabbitTemplate.setMandatory(true);

    //设置ReturnCallBack
      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("return执行了....");
              System.out.println(message);
              System.out.println(replyCode);
              System.out.println(replyText);
              System.out.println(exchange);
              System.out.println(routingKey);

              //错误之后的逻辑处理
          }
      });

    rabbitTemplate.convertAndSend("directs","","发送info的key的路由消息");
}

我们这里故意把routingKey设为了空,人为的制造了错误

测试结果如下:

总结:

  • 设置publisher-confirm-type=correlated 开启确认模式
  • 使用rabbitTemplate.setConfirmCallback设置回调函数.当消息发送到exchange后回调confirm方法.在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理

Consumer Ack

ack指Acknowledge,确认.表示消费端收到消息后的确认方式

有三种确认方式:

  • 自动确认: acknowledge="none"
  • 手动确认: acknowledge="manual"
  • 根据异常情况确认: acknowledge="auto"(不建议使用)

自动确认是指: 当消息一旦被Consumer接收到,则自动确认,并将相应的message从RabbitMQ的消息缓存中移除.但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失.

如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel,basicNack()方法,让其自动重新发送消息

测试代码如下:

/**
 * 1. 设置手动签收  ackMode="manual"
 * 2. 如果消息成功处理,则调用channel的basicAck()方法
 * 3. 如果消息处理失败,则调用channel的basicNack()拒绝签收,然后broker重新发送给consumer
 * @param message
 */
@RabbitListener(bindings = {
        @QueueBinding(
                value = @Queue,  //创建临时队列
                exchange = @Exchange(value = "directs",type = "direct"),  //绑定交换机
                key = {"info"}  //指定路由的key
        )
},ackMode = "MANUAL")  //设置手动签收模式
public void receiveByAck(Message message, Channel channel) throws IOException {
    long deliveryTag = message.getMessageProperties().getDeliveryTag();
    try {
        System.out.println("message = " + new String(message.getBody()));

        System.out.println("处理业务逻辑");
        int i = 3/0;  //模拟业务逻辑出现错误

        /**
         * 参数1: 传递过来的消息的标签
         * 参数2: 是否接受多条消息
         */
        channel.basicAck(deliveryTag,true);
    } catch (Exception e) {
        /**
         * 拒绝签收
         * 参数3: 重回队列,如果设置为true,则消息重新回到queue,broker会重新发送给客户端
         */
        channel.basicNack(deliveryTag,true,true);
    }
}

当出现错误后的结果:

当业务逻辑恢复正常后的结果

消息可靠性的四个方面

  1. 消息持久化
    • exchange要持久化
    • queue要持久化
    • message要持久化
  2. 生产方确认 Confirm
  3. 消息方确认 Ack
  4. Broker高可用 镜像集群搭建

消费端限流

image-20210219023003482

在配置文件中配置

spring.rabbitmq.listener.simple.prefetch=15

测试代码

/**
 * @PROJECT_NAME: myTest
 * @DESCRIPTION:  消费者
 * 1. 确保ack机制为手动确认
 * 2. 在配置文件中配置  spring.rabbitmq.listener.direct.prefetch=1
 *      - 表示消费端每次从mq拉取一条消息来消费,直到手动确认消费完毕后,才会继续拉取下一条
 * @USER: 罗龙达
 * @DATE: 2021/2/17 1:54
 */
@Component
public class HelloConsumer {


    @RabbitListener(queuesToDeclare = @Queue(value = "hello"),ackMode = "MANUAL")
    public void receive(Message message, Channel channel) throws IOException, InterruptedException {

        System.out.println("message = " + new String(message.getBody()));

        System.out.println("处理业务逻辑..");
        Thread.sleep(1000);

        channel.basicAck(message.getMessageProperties().getDeliveryTag(),true); //手动确认消息

    }
}

这样consumer就会15条15条的拉取消息


TTL

  • TTL全称 Time To Live(存活时间)
  • 当消息到达存活时间后,还没有被消费,会被自动清除
  • RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间

设置过期队列测试代码如下:

@RabbitListener(queuesToDeclare = @Queue(value = "test_queue_ttl", 
        arguments = @Argument(name = "x-message-ttl",value = "10000",type = "java.lang.Long"))) //设置队列的ttl属性  type记得改成long类型
public void receiveTTl(Message message, Channel channel) throws IOException, InterruptedException {

    System.out.println("message = " + new String(message.getBody()));

    System.out.println("处理业务逻辑..");
    Thread.sleep(1000);

    channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);

}

消息单独过期测试代码如下:

@Test
public void testTopic() {
    rabbitTemplate.convertAndSend("topics", "delete.order", "基于delete.order的路由消息",new MessagePostProcessor() {
        /**
         *设置消息单独过期的方法
         * 如果设置了消息的过期时间,也设置了队列的过期时间,它以时间短的为准.
         * 队列过期后,会将所有的消息全部移除
         * 消息过期后,只有消息在队列顶端,才会判断其是否过期(移除掉)
         */
        @Override
        public Message postProcessMessage(Message message) throws AmqpException {
            message.getMessageProperties().setExpiration("5000");
            return message;
        }
    });
}

死信队列

死信队列:英文缩写:DLX.

Dead Letter Exchange(死信交换机),当消息成为Dead message后,可以被重新发送到拎一个交换机,这个交换机就是DLX

消息成为死信的三种情况:

  1. 队列消息长度达到限制
  2. 消费者拒绝消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
  3. 原队列存在消息过期设置,消息到达超时时间未被消费

队列绑定死信交换机:

​ 给队列设置参数:x-dead-letter-exchangex-dead-letter-routing-key

测试代码如下:

    @RabbitListener(bindings = {
            @QueueBinding(
                    value = @Queue(value = "test_DLX",
                                   arguments = {  //这里的argument一定不要加错了地方!!
                            @Argument(name = "x-dead-letter-exchange",value = "DLX"),  //指定死信交换机
                            @Argument(name = "x-dead-letter-routing-key",value = "deadKey"),  //指定死信交换机的routingkey
                            @Argument(name = "x-message-ttl",value = "5000",type = "java.lang.Long"),  //指定消息过期时间
                            @Argument(name = "x-max-length",value = "5",type = "java.lang.Long")  //指定最大长度,当发送的消息超过了这个数就会进入死信队列),  //创建临时队列
                    }
                    ), 
                    exchange = @Exchange(value = "directs",type = "direct"),  //绑定交换机
                    key = {"info","warning","error"} //指定路由的key
                    
            )
    })
    public void receiveDLX(String message){
        System.out.println("message1 = " + message);
    }

总结:

  1. 死信交换机和死信队列和普通的交换机队列没啥区别
  2. 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列
  3. 消息成为私心的三种情况
    1. 队列消息长度达到限制
    2. 消费者拒绝消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
    3. 原队列存在消息过期设置,消息到达超时时间未被消费

延迟队列

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费

需求:

1. 下单后,30分钟未支付,取消订单,回滚库存
2. 新用户注册成功7天后,发送短信问候

实现方式:

1. 定时器(创建订单的时候同时上传创建时间,写一段代码以轮询的方式去访问库表,当前时间与创建时间差值在30分钟以上的就删除订单)
2. 延迟队列

但是在RabbitMQ中并未提供延迟队列功能...

但是可以使用:TTL + 死信队列组合实现延迟队列的效果

测试代码如下:

    @RabbitListener(bindings = {
            @QueueBinding(
                    value = @Queue(value = "test_Delay",arguments = {
                            @Argument(name = "x-dead-letter-exchange",value = "DeadLetterEx"),  //指定死信交换机
                            @Argument(name = "x-dead-letter-routing-key",value = "cancel"),  //指定死信交换机的routingKey
                            @Argument(name = "x-message-ttl",value = "10000",type = "java.lang.Long"), //超时时间为10s
                            @Argument(name = "x-max-length",value = "3",type = "java.lang.Long")  //队列最大长度为3
                    }),  //创建普通队列
                    exchange = @Exchange(value = "test_delay_exchange",type = "direct"),  //绑定交换机
                    key = {"info","warning","error"}  //指定路由的key
            )
    },ackMode = "MANUAL")
    public void receiveDelay(Message message, Channel channel) throws IOException {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            System.out.println("message = " + new String(message.getBody()));

            System.out.println("处理业务逻辑");
            int i = 3/0;  //模拟业务逻辑出现错误

            /**
             * 参数1: 传递过来的消息的标签
             * 参数2: 是否接受多条消息
             */
            channel.basicAck(deliveryTag,true);
            System.out.println("业务逻辑处理完毕");
        } catch (Exception e) {
            /**
             * 拒绝签收
             * 参数3: 重回队列,如果设置为true,则消息重新回到queue,broker会重新发送给客户端
             */
            System.out.println("执行拒绝签收");
            channel.basicNack(deliveryTag,true,false);
        }
    }

    @RabbitListener(bindings = {
            @QueueBinding(
                    value = @Queue("receiveOrder"),  //创建一个队列,监听死信交换机
                    exchange = @Exchange(value = "DeadLetterEx",type = "direct"),
                    key = {"cancel"})
    })
    public void receiveOrder(String message){
        System.out.println("判断订单状态 " + message + new Date());
    }

消息可靠性保障

需求:如何保证消息100%传递成功?

一开始的业务逻辑可能是这样的

生产者发送消息到消息队列,消费者消费。但是如果producer的业务数据入库了,但是发送消息失败了,consumer就接受不到消息了。因此我们的架构需要改进

如果说前面那条消息consumer接收成功了,就发送确认消息到Q2队列里,并调用回调服务写到另一个检测数据库里,

当producer发完消息后的一段时间里再向Q3里发送一条一模一样的消息,并通过回调服务拿到检测数据库里去比对,比对这条消息跟刚才发送的那条消息的id,
如果有这条消息的id代表consumer正常接收消息,就什么都不做,如果找不到匹配的消息,就让producer重新发一次消息

假设极端情况,发送消息失败了,发送延迟消息也失败了,这个时候架构又得改进

添加一个定时检查服务,每个几个小时就看一看MDB和DB里的消息id是否一致,不匹配的就让producer重新发送消息

消息幂等性保障

幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说。其任意多次执行对资源本身所产生的影响均与依次执行的影响相同。

​ 在MQ中指,消费多条相同的消息,得到与消费该消息一次相同的结果

乐观锁机制

比如说consumer宕机了几分钟Q1队列里堆积了两条或者多条消息,如果不做幂等性保障,可能会导致多次扣款等情况发生,因此我们加入version版本号

第一次执行:version=1

update account set money = money -500, version = version + 1 ,where id = 1 and version = 1;

第二次执行:version=2

update account set money = money -500, version = version + 1 ,where id = 1 and version = 1;

这个时候数据库中没有匹配的记录,update就不会执行,这样就保障了一条消息不会重复执行


posted @ 2021-03-09 02:18  longda666  阅读(315)  评论(0)    收藏  举报