RabbitMq(二)

六、死信队列

6.1 死信的概念

死信,顾名思义死掉的消息,就是无法被消费的消息。

producer 将消息投递到 broker 或者直接到queue 里了,consumer 从 queue 取出消息进行消费。

但某些时候由于特定的原因导致 queue 中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信自然就有了死信队列。

6.2 死信的来源

三种来源:

  • 消息 TTL 过期
  • 队列达到最大长度(队列满了,无法再添加数据到 mq 中)
  • 消息被拒绝(basic.reject 或 basic.nack)并且 requeue=false.

6.3 死信队列演示

6.3.1 生产者

设置队列属性:

  • 消息ttl
  • 队列最大长度
public class Producer {
    private static final String NORMAL_EXCHANGE = "normal_exchange";
    private static final String DEAD_EXCHANGE = "dead_exchange";
    private static final String NORMAL_QUEUE = "normal_queue";
    private static final String DEAD_QUEUE = "dead_queue";
    private static final String NORMAL_ROUTING = "normal";
    private static final String DEAD_ROUTING = "dead";
    private static final Long TTL = 10000L;
    private static final Long LENGTH = 8L;

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = RabbitMQUtil.getConnection();
        Channel channel = RabbitMQUtil.getChannel(connection);

        //声明正常交换机
        channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
        //声明死信交换机
        channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);

        HashMap<String, Object> arguments = new HashMap<>();
        //消息超时时间:超时未消费将会进入死信队列
        arguments.put("x-message-ttl", TTL);
        //队列长度:超过长度消息将会进入死信队列
        arguments.put("x-max-length", LENGTH);
        //死信交换机
        arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE);
        //死信路由键
        arguments.put("x-dead-letter-routing-key", DEAD_ROUTING);
        //声明正常队列
        channel.queueDeclare(NORMAL_QUEUE, true, false, false, arguments);
        //声明死信队列
        channel.queueDeclare(DEAD_QUEUE, true, false, false, null);

        //绑定队列关系
        channel.queueBind(NORMAL_QUEUE, NORMAL_EXCHANGE, NORMAL_ROUTING);
        channel.queueBind(DEAD_QUEUE, DEAD_EXCHANGE, DEAD_ROUTING);


        for (int i = 1; i <= 10; i++) {
            String message = "info" + i;
            channel.basicPublish(NORMAL_EXCHANGE, NORMAL_ROUTING, null, message.getBytes(StandardCharsets.UTF_8));
        }


        channel.close();
        connection.close();
    }
}

6.3.2 正常队列消费者

正常消费即可,模拟特殊消息无法被消费且不重入队列

public class Consumer1 {
    private static final String NORMAL_QUEUE = "normal_queue";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Connection connection = RabbitMQUtil.getConnection();
        Channel channel = RabbitMQUtil.getChannel(connection);

        DeliverCallback deliverCallback = (consumerTag, message) -> {
            String s = new String(message.getBody());
            System.out.println("接收消息:" + s);
            if (s.equals("info5")) {
                //拒收且不重入队列,则会进入死信队列
                System.out.println("拒收消息:" + s);
                channel.basicReject(message.getEnvelope().getDeliveryTag(), false);
            } else {
                channel.basicAck(message.getEnvelope().getDeliveryTag(), false);
            }
        };
        CancelCallback cancelCallback = (consumerTag) -> {};
        channel.basicConsume(NORMAL_QUEUE, false, deliverCallback, cancelCallback);


        Thread.sleep(10000);
        channel.close();
        connection.close();
    }
}

6.3.3 死信队列消费者

直接消费死信队列中的消息,同普通消费者一样。

public class Consumer2 {
    private static final String DEAD_QUEUE = "dead_queue";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        Connection connection = RabbitMQUtil.getConnection();
        Channel channel = RabbitMQUtil.getChannel(connection);

        DeliverCallback deliverCallback = (consumerTag, message) -> {
            String s = new String(message.getBody());
            System.out.println("接收消息:" + s);
            channel.basicAck(message.getEnvelope().getDeliveryTag(), false);

        };
        CancelCallback cancelCallback = (consumerTag) -> {
        };
        channel.basicConsume(DEAD_QUEUE, false, deliverCallback, cancelCallback);


        Thread.sleep(10000);
        channel.close();
        connection.close();
    }
}

6.3.4 演示结果

满足死信条件的消息都将进入死信队列,被死信消费者消费。

1、超过队列长度

队列长度最大为8,发送10条消息,超过队列长度的消息进入死信队列。

2、消息超时未被消费

消息超时时间为10s,超过10s未被消费后,进入死信队列。

3、消息被拒

消息若被消费者拒绝消费,且不重入队列,则也会进入死信队列。

4、死信队列消费结果

七、延迟队列

7.1 延迟队列概念

延时队列就是用来存放需要在指定时间被处理的元素的队列。

7.2 延迟队列使用场景

  • 订单在十分钟之内未支付则自动取消
  • 新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
  • 用户注册成功后,如果三天内没有登陆则进行短信提醒。
  • 用户发起退款,如果三天内没有得到处理则通知相关运营人员。
  • 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议

这些场景都有一个特点,需要在某个事件发生之后或者之前的指定时间点完成某一项任务。

如: 订购车票事件,在30分钟之后检查该订单支付状态,然后将未支付的订单进行关闭;

看起来似乎使用定时任务,一直轮询数据,每秒查一次,取出需要被处理的数据,然后处理不就完事了吗?

如果数据量比较少,确实可以这样做,比如:对于“如果账单一周内未支付则进行自动结算”这样的需求, 如果对于时间不是严格限制,而是宽松意义上的一周,那么每天晚上跑个定时任务检查一下所有未支付的账单,确实也是一个可行的方案。

但对于数据量比较大,并且时效性较强的场景,如:“订单30分钟内未支付则关闭“,短期内未支付的订单数据可能会有很多,活动期间甚至会达到百万甚至千万级别。

对这么庞大的数据量仍旧使用轮询的方式显然是不可取的,很可能在一秒内无法完成所有订单的检查,同时会给数据库带来很大压力,无法满足业务要求而且性能低下。

考虑使用延迟队列:

7.3 RabbitMQ中的TTL

TTL 是 RabbitMQ 中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间, 单位是毫秒。

换句话说,如果一条消息设置了 TTL 属性或者进入了设置TTL 属性的队列,那么这条消息如果在TTL 设置的时间内没有被消费,则会成为"死信"。

如果同时配置了队列的TTL 和消息的 TTL,那么较小的那个值将会被使用,有两种方式设置 TTL。

消息设置TTL

MessagePostProcessor messagePostProcessor = message -> {
    message.getMessageProperties().setExpiration(ttl);
    return message;
};

队列设置TTL

@Bean("queueA")
public Queue queueA() {
    
    return QueueBuilder.durable(QUEUE_A)
            .ttl(QUEUE_A_TTL)       // 设置队列属性x-message-ttl
            .deadLetterExchange(EXCHANGE_DEAD_LETTER_Y)
            .deadLetterRoutingKey(Y_D_ROUTING_KEY)
            .build();
}

消息TTL和队列TTL区别

  • 设置了队列的 TTL 属性,那么一旦消息过期,就会被队列丢弃(如果配置了死信队列被丢到死信队 列中)。
  • 设置了消息的 TTL 属性,消息即使过期,也不一定会被马上丢弃。
  • 消息是否过期是在即将投递到消费者之前判定的,如果当前队列有严重的消息积压情况,则已过期的消息也许还能存活较长时间
  • 需要注意的一点是,如果不设置 TTL,表示消息永远不会过期。如果将 TTL 设置为 0,则表示除非此时可以直接投递该消息到消费者,否则该消息将会被丢弃。

7.4 整合SpringBoot

Demo地址:https://github.com/user0819/rabbitmq-springboot.git

7.4.1 创建SpringBoot项目

spring-boot 推荐使用2.5版本,适配swagger2版本。

7.4.2 添加依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </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>

7.4.3 修改配置项

spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin

7.4.4 swagger配置类

(可选)

@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")
                .build();
    }
}

7.5 队列TTL

7.5.1 消息队列结构图

  • 创建两个队列 QA 和 QB,队列 TTL 分别设置为 10S 和 40S。
  • 创建交换机 X,类型是direct
  • 创建死信队列 QD。
  • 创建死信交换机 Y,类型是direct

它们的绑定关系如下:

7.5.2 配置文件类代码

@Configuration
public class TtlQueueConfig {
    public static final String EXCHANGE_X = "X";
    public static final String QUEUE_A = "QA";
    public static final String QUEUE_B = "QB";
    public static final String X_A_ROUTING_KEY = "XA";
    public static final String X_B_ROUTING_KEY = "XB";
    public static final int QUEUE_A_TTL = 10000;
    public static final int QUEUE_B_TTL = 20000;


    public static final String EXCHANGE_DEAD_LETTER_Y = "Y";
    public static final String QUEUE_DEAD_LETTER_D = "QD";
    public static final String Y_D_ROUTING_KEY = "YD";


    /**
     * 声明交换机 xExchange
     */
    @Bean("xExchange")
    public DirectExchange xExchange() {
        return new DirectExchange(EXCHANGE_X);
    }

    /**
     * 声明死信交换机 yExchange
     */
    @Bean("yExchange")
    public DirectExchange yExchange() {
        return new DirectExchange(EXCHANGE_DEAD_LETTER_Y);
    }

    /**
     * 声明队列 A 
     * ttl 为 10s 
     * 并绑定到对应的死信交换机
     */
    @Bean("queueA")
    public Queue queueA() {
        return QueueBuilder.durable(QUEUE_A)
                .ttl(QUEUE_A_TTL)
                .deadLetterExchange(EXCHANGE_DEAD_LETTER_Y)
                .deadLetterRoutingKey(Y_D_ROUTING_KEY)
                .build();
    }

    /**
     * 声明队列 A 绑定 X 交换机
     */
    @Bean
    public Binding queueABindingX(@Qualifier("queueA") Queue queueA, @Qualifier("xExchange") DirectExchange xExchange) {
        return BindingBuilder.bind(queueA).to(xExchange).with(X_A_ROUTING_KEY);
    }

    
    /**
     * 声明队列 B
     * ttl 为 40s
     * 并绑定到对应的死信交换机
     */
    @Bean("queueB")
    public Queue queueB() {
        return QueueBuilder.durable(QUEUE_B)
                .ttl(QUEUE_B_TTL)
                .deadLetterExchange(EXCHANGE_DEAD_LETTER_Y)
                .deadLetterRoutingKey(Y_D_ROUTING_KEY)
                .build();
    }

    /**
     * 声明队列 B 绑定 X 交换机
     */
    @Bean
    public Binding queueBBindingX(@Qualifier("queueB") Queue queue1B, @Qualifier("xExchange") DirectExchange xExchange) {
        return BindingBuilder.bind(queue1B).to(xExchange).with(X_B_ROUTING_KEY);
    }

    /**
     * 声明死信队列 QD
     */
    @Bean("queueD")
    public Queue queueD() {
        return new Queue(QUEUE_DEAD_LETTER_D);
    }

    /**
     * 声明死信队列 QD 绑定关系
     */
    @Bean
    public Binding deadLetterBindingQAD(@Qualifier("queueD") Queue queueD, @Qualifier("yExchange") DirectExchange yExchange) {
        return BindingBuilder.bind(queueD).to(yExchange).with(Y_D_ROUTING_KEY);
    }
}

7.5.3 生产者代码

@GetMapping(value = "/sendMsg/{msg}")
public String sendMsg(@PathVariable String msg) {
    log.info("当前时间:{},发送消息:{}", LocalDateTime.now(), msg);
    rabbitTemplate.convertAndSend(TtlQueueConfig.EXCHANGE_X, TtlQueueConfig.X_A_ROUTING_KEY, msg);
    rabbitTemplate.convertAndSend(TtlQueueConfig.EXCHANGE_X, TtlQueueConfig.X_B_ROUTING_KEY, msg);
    return "success";
}

7.5.4 消费者代码

//消费死信队列消息
@RabbitListener(queues = "QD")
public void consume(Message message, Channel channel) {
    String msg = new String(message.getBody());
    log.info("当前时间:{}, 消费者收到消息:{}", LocalDateTime.now(), msg);
}

发送请求后,消费结果:

  • 第一条消息在 10S 后变成了死信消息,然后被消费者消费掉。
  • 第二条消息在 20S 之后变成了死信消息,然后被消费掉,这样一个延时队列就打造完成了。

7.6 延迟队列优化

上述代码使用方式存在的问题:

每增加一个新的时间需求,就要新增一个队列。这里只有 10S 和 20S 两个时间选项。

如果需要一个小时后处理,那么就需要增加TTL 为一个小时的队列。

如果是预定会议室然后提前通知这样的场景,要增加n个队列才能满足需求。

7.6.1 消息队列结构图

在这里新增了一个队列 QC,绑定关系如下:

该队列不设置TTL 时间,而是设置消息 TTL。

7.6.2 配置文件类代码

//...
public static final String QUEUE_C = "QC";
public static final String X_C_ROUTING_KEY = "XC";
//...

/**
 * 声明队列C
 * 绑定死信队列。
 * 队列C没有设置ttl,过期时间由消息自身决定。
 */
@Bean("queueC")
public Queue queueC() {
    return QueueBuilder.durable(QUEUE_C)
            .deadLetterExchange(EXCHANGE_DEAD_LETTER_Y)
            .deadLetterRoutingKey(Y_D_ROUTING_KEY)
            .build();
}

/**
 *  声明队列 C 绑定 X 交换机
 */
@Bean
public Binding queueCBindingX(@Qualifier("queueC") Queue queueC, @Qualifier("xExchange") DirectExchange xExchange) {
    return BindingBuilder.bind(queueC).to(xExchange).with(X_C_ROUTING_KEY);
}

7.6.3 生产者代码

@GetMapping(value = "/sendTtlMsg/{msg}/{ttl}")
public String sendMsg(@PathVariable String msg, @PathVariable String ttl) {
    log.info("当前时间:{},发送消息:{}, ttl:{}", LocalDateTime.now(), msg, ttl);
    MessagePostProcessor messagePostProcessor = message -> {
        //ttl单位是ms
        message.getMessageProperties().setExpiration(ttl);
        return message;
    };
    rabbitTemplate.convertAndSend(TtlQueueConfig.EXCHANGE_X, TtlQueueConfig.X_C_ROUTING_KEY, msg, messagePostProcessor);
    return "success";
}

发送两次请求后,消费结果:

http://localhost:8080/ttl/sendTtlMsg/嘻嘻嘻1/10000

http://localhost:8080/ttl/sendTtlMsg/嘻嘻嘻2/1000

  • 第一条消息在 10S 后过期后变成了死信消息,然后被消费者消费掉。(没问题)
  • 第二条消息预期在1s后变成死信消息,但实际却是在第一条消息过期后,第二条消息才过期变成死信消息,然后被消费者消费掉。(有问题:没有按时死亡)

7.7 RabbitMQ插件实现延迟队列

上述方式问题:

因为 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,。

如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。

如果不能实现在消息粒度上的 TTL,并使其在设置的TTL 时间及时死亡,就无法设计成一个通用的延时队列。

需要使用插件去实现可靠通用的延迟队列。

7.7.1 安装延迟队列插件

1、官网:https://www.rabbitmq.com/community-plugins.html,下载 rabbitmq_delayed_message_exchange 插件

2、放置到 RabbitMQ 的插件目录

3、进入 RabbitMQ 的安装目录下的 plugins 目录,执行下面命令让该插件生效,

rabbitmq-plugins enable rabbitmq_delayed_message_exchang

4、重启 RabbitMQ

(好像也可以不用重启)

  1. 登录管理页面,查看是否已有新的交换机类型

7.7.2 消息队列结构图

新增一个队列delayed.queue,一个自定义交换机 delayed.exchange,绑定关系如下:

7.7.3 配置文件类代码

在自定义的交换机中,这是一种新的交换类型。

该类型消息支持延迟投递机制消息传递后并不会立即投递到目标队列中,而是存储在 mnesia(一个分布式数据系统)表中,当达到投递时间时,才投递到目标队列中。

@Configuration
public class DelayedQueueConfig {
    public static final String DELAYED_QUEUE_NAME = "delayed.queue";
    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
    public static final String DELAYED_ROUTING_KEY = "delayed.routingKey";

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

    @Bean
    public CustomExchange delayedExchange() {
        //自定义交换机的类型
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");
        return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false, args);
    }

    @Bean
    public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue, @Qualifier("delayedExchange") CustomExchange delayedExchange) {
        return BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
    }
}

7.7.4 生产者代码

@GetMapping("/sendDelayMsg/{msg}/{delay}")
public String sendMsg(@PathVariable String msg, @PathVariable Integer delay) {
    log.info("当前时间:{},发送消息:{}, delay:{}", LocalDateTime.now(), msg, delay);
    MessagePostProcessor messagePostProcessor = message -> {
        //delay单位是ms
        message.getMessageProperties().setDelay(delay);
        return message;
    };
    rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME, DelayedQueueConfig.DELAYED_ROUTING_KEY, msg, messagePostProcessor);
    return "success";
}

7.7.5 消费者代码

//消费插件延迟队列
@RabbitListener(queues = "delayed.queue")
public void receiveDelayedQueue(Message message) {
    String msg = new String(message.getBody());
    log.info("当前时间:{},收到延时队列的消息:{}", LocalDateTime.now(), msg);
}

发送两次请求后,消费结果:

http://localhost:8080/ttl/sendDelayMsg/哈哈哈1/10000

http://localhost:8080/ttl/sendDelayMsg/哈哈哈2/1000

  • 第一条消息在 10s 过期后变成了死信消息,然后被消费者消费掉。
  • 第二条消息在 1s 过期后变成了死信消息,然后被消费者消费掉。

第二个消息过期早,被先消费掉了,符合预期。

7.8 总结

7.8.1 RabbitMq延迟队列方式

  • 基于死信队列的延迟队列
    • 队列设置ttl
    • 消息设置ttl
  • 基于插件的延迟队列

7.8.2 其他延迟队列方案

  • Java 的 DelayQueue
  • Redis 的 zset
  • Quartz
  • kafka 的时间轮,

八、发布确认高级

在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败, 导致消息丢失,需要手动处理和恢复。

8.1 发布确认springboot版本

(同之前说的单条批量异步确认机制,此处提供springboot下确认机制)

8.1.1 确认机制方案

8.1.2 交换机队列关系

8.1.3 配置文件

在配置文件当中需要添加

spring.rabbitmq.publisher-confirm-type=correlated

  • NONE:禁用发布确认模式,是默认值
  • CORRELATED:发布消息成功到交换器后会触发回调方法
  • SIMPLE:经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法, 其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法 等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是 waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel,则接下来无法发送消息到 broker

8.1.4 添加配置类

@Configuration
public class ConfirmConfig {

    public static final String EXCHANGE_NAME = "confirm.exchange";
    public static final String QUEUE_NAME = "confirm.queue";
    public static final String ROUTING_KEY = "key1";


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

    @Bean("confirmQueue")
    public Queue confirmQueue() {
        return QueueBuilder.durable(QUEUE_NAME).build();
    }

    @Bean
    public Binding binding(@Qualifier("confirmExchange") DirectExchange directExchange, @Qualifier("confirmQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(directExchange).with(ROUTING_KEY);
    }
}

8.1.5 消息生产者

@Slf4j
@RestController
@RequestMapping("/confirm")
public class ConfirmController {
    public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private MyCallBack myCallBack;

    //依赖注入 rabbitTemplate 之后再设置它的回调对象
    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(myCallBack);

        //交换机无法将消息进行路由时,会将该消息返回给生产者
        //如果发现消息无法进行路由,则直接丢弃
        rabbitTemplate.setMandatory(true);
        //设置回退消息交给谁处理
        rabbitTemplate.setReturnCallback(myCallBack);
    }


    @GetMapping("/sendMsg/{message}")
    public String produce(@PathVariable String message) {

        //指定消息 id 为 1
        CorrelationData correlationData1 = new CorrelationData("1");
        String routingKey = "key1";
        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey, message + routingKey, correlationData1);

        CorrelationData correlationData2 = new CorrelationData("2");
        routingKey = "key2";
        rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME, routingKey, message + routingKey, correlationData2);
        log.info("发送消息内容:{}", message);

        return "success";
    }
}

8.1.6 回调接口

@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {
    /**
     * 交换机不管是否收到消息的一个回调方法
     * CorrelationData
     * 消息相关数据
     * ack
     * 交换机是否收到消息
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String id = correlationData != null ? correlationData.getId() : "";
        if (ack) {
            log.info("交换机已经收到 id 为:{}的消息", id);
        } else {
            log.info("交换机还未收到 id 为:{}消息,由于原因:{}", id, cause);
        }
    }

    //当消息无法路由的时候的回调方法
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        log.error("消息{}, 被交换机{} 退回,退回原因:{}, 路由key:{}", new String(message.getBody()), exchange, replyText, routingKey);
    }
}

8.1.7 消息消费者

@Slf4j
@Component
public class ConfirmConsumer {

    //消费死信队列消息
    @RabbitListener(queues = "confirm.queue")
    public void consume(Message message) {
        String msg = new String(message.getBody());
        log.info("当前时间:{}, 消费者收到消息:{}", LocalDateTime.now(), msg);
    }
}

8.1.8 结果分析

8.2 备份交换机

当交换机无法投递某个消息,又不想丢失这个消息时,可以为交换机设置备份备份交换机,将无法处理的消息交由备份交换机处理。

8.2.1代码架构图

8.2.2 修改配置类

  • 声明交换机的备份交换机
  • 绑定备份交换机的队列关系
@Configuration
public class ConfirmConfig {

    public static final String EXCHANGE_NAME = "confirm.exchange";
    public static final String QUEUE_NAME = "confirm.queue";
    public static final String ROUTING_KEY = "key1";

    public static final String BACKUP_EXCHANGE_NAME = "backup.exchange";
    public static final String BACKUP_QUEUE_NAME = "backup.queue";
    public static final String WARNING_QUEUE_NAME = "warning.queue";

    @Bean("confirmQueue")
    public Queue confirmQueue() {
        return QueueBuilder.durable(QUEUE_NAME).build();
    }

    //声明确认 Exchange 交换机的备份交换机
    @Bean("confirmExchange")
    public DirectExchange confirmExchange() {
        ExchangeBuilder exchangeBuilder = ExchangeBuilder.directExchange(EXCHANGE_NAME).durable(true).withArgument("alternate-exchange", BACKUP_EXCHANGE_NAME);
        return exchangeBuilder.build();
    }

    @Bean
    public Binding binding(@Qualifier("confirmExchange") DirectExchange directExchange, @Qualifier("confirmQueue") Queue queue) {
        return BindingBuilder.bind(queue).to(directExchange).with(ROUTING_KEY);
    }

    //声明备份 Exchange
    @Bean("backupExchange")
    public FanoutExchange backupExchange() {
        return new FanoutExchange(BACKUP_EXCHANGE_NAME);
    }


    // 声明警告队列
    @Bean("warningQueue")
    public Queue warningQueue() {
        return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
    }

    // 声明备份队列
    @Bean("backQueue")
    public Queue backQueue() {
        return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
    }

    // 声明报警队列绑定关系
    @Bean
    public Binding warningBinding(@Qualifier("warningQueue") Queue queue, @Qualifier("backupExchange") FanoutExchange backupExchange) {
        return BindingBuilder.bind(queue).to(backupExchange);
    }

    // 声明备份队列绑定关系
    @Bean
    public Binding backupBinding(@Qualifier("backQueue") Queue queue, @Qualifier("backupExchange") FanoutExchange backupExchange) {
        return BindingBuilder.bind(queue).to(backupExchange);
    }
}

8.2.3 报警消费类

当confirm.exchange交换机处理不了消息时,就会由备份交换机路由发送到告警队列。

然后被消费。

@RabbitListener(queues = WARNING_QUEUE_NAME)
public void receiveWarningMsg(Message message) {
    String msg = new String(message.getBody());
    log.error("报警发现不可路由消息:{}", msg);
}

8.2.4 结果分析

mandatory 参数与备份交换机可以一起使用的时候,如果两者同时开启,备份交换机优先级高。

九、其他知识点

9.1 幂等性

9.1.1 概念

用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。

9.1.2 消息重复消费

消费者在消费 MQ 中的消息时,MQ 已把消息发送给消费者,消费者在给MQ 返回 ack 时网络中断,故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。

9.1.3 解决思路

MQ 消费者的幂等性的解决一般使用全局 ID 或者写个唯一标识比如时间戳 或者 UUID 或者订单消费者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消息时用该 id 先判断该消息是否已消费过。

9.1.4 消费端的幂等性保障

在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性,这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:

  • a.唯一 ID+指纹码机制,利用数据库主键去重
  • b.利用 redis 的原子性去实现

9.1.5 唯一ID + 指纹码机制

指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id 是否存在数据库中.

  • 优势就是实现简单就一个拼接,然后查询判断是否重复;
  • 劣势就是在高并发时,如果是单个数据库就会有写入性能瓶颈。当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。

9.1.6 Redis原子性

利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费

9.2 优先级队列

9.2.1 使用场景

某些消息,需要优先被消费。

比如大客户的订单,需要优先催付等。

9.2.2 如何添加

1、创建优先队列,设置队列最大支持的优先级

2、消息中添加优先级

AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
channel.basicPublish("", QUEUE_NAME, properties, message.getBytes());

9.2.3 实战

生产者

public static void main(String[] args) throws Exception {
    try (Channel channel = RabbitMQUtil.getChannel();) {

        for (int i = 1; i < 11; i++) {
            String message = "info" + i;
            if (i == 5) {
                AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
                channel.basicPublish("", QUEUE_NAME, properties, message.getBytes());
            } else {
                channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            }
            System.out.println("发送消息完成:" + message);
        }
    }
}

消费者

public static void main(String[] args) throws Exception {
    Channel channel = RabbitMQUtil.getChannel();
    //设置队列的最大优先级 最大可以设置到 255 官网推荐 1-10 如果设置太高比较吃内存和 CPU
    Map<String, Object> params = new HashMap<>();
    params.put("x-max-priority", 10);
    channel.queueDeclare(QUEUE_NAME, true, false, false, params);
    System.out.println("消费者启动等待消费..............");
    DeliverCallback deliverCallback = (consumerTag, delivery)
            -> {
        String receivedMessage = new
                String(delivery.getBody());
        System.out.println("接收到消息:" + receivedMessage);
    };
    channel.basicConsume(QUEUE_NAME, true, deliverCallback, (consumerTag) -> {
        System.out.println("消费者无法消费消息时调用,如队列被删除");
    });
}

9.3 惰性队列

9.3.1 使用场景

惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,目标是能够支持更长的队列支持更多的消息存储。

默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存之中,这样可以更加快速的将消息发送给消费者。

9.3.2 两种模式

队列具备两种模式:default 和 lazy。默认的为default 模式,在3.6.0 之前的版本无需做任何变更。

lazy模式即为惰性队列的模式,可以通过调用 channel.queueDeclare 方法的时候在参数中设置,也可以通过Policy 的方式设置,如果一个队列同时使用这两种方式设置的话,那么 Policy 的方式具备更高的优先级。

在队列声明的时候可以通过“x-queue-mode”参数来设置队列的模式,取值为“default”和“lazy”。

下面示例中演示了一个惰性队列的声明细节:

Map args = new HashMap(); 
args.put("x-queue-mode", "lazy"); 
channel.queueDeclare("myqueue", false, false, false, args);

9.3.3 内存开销占比

在发送 1 百万条消息,每条消息大概占 1KB 的情况下,普通队列占用内存是 1.2GB,而惰性队列仅仅占用 1.5MB。

十、集群

10.1 集群

10.1.1 为什么使用集群

如果 RabbitMQ 服务器遇到内存崩溃、机器掉电或者主板故障等情况,该怎么办?单台 RabbitMQ 服务器可以满足每秒 1000 条消息的吞吐量,那么如果应用需要 RabbitMQ 服务满足每秒 10 万条消息的吞吐量呢?购买昂贵的服务器来增强单机 RabbitMQ 务的性能显得捉襟见肘,搭建一个 RabbitMQ 集群才是解决实际问题的关键.

10.1.2 搭建步骤

以搭建三台机器集群说明。

首先在三台服务器上分别安装rabbitmq,然后搭建集群。

1、修改3台机器的主机名称

vi /etc/hostname

2、配置各个节点的hosts文件,让各节点都能识别对方

vi /etc/hosts

10.21.41.38 node1
10.21.41.39 node2
10.21.41.40 node3

3、确保各个节点的cookie文件使用的同一个值

将master的cookie同步给其他两个节点。

scp /var/lib/rabbitmq/.erlang.cookie root@node2:/var/lib/rabbitmq/.erlang.cookie 
scp /var/lib/rabbitmq/.erlang.cookie root@nod23:/var/lib/rabbitmq/.erlang.cookie

4、启动 RabbitMQ 服务,顺带启动 Erlang 虚拟机和 RbbitMQ 应用服务。(在三台节点上分别执行以 下命令)

rabbitmq-server -detached

5、在节点2、3上启动服务,并加入到node1的集群

rabbitmqctl stop_app (rabbitmqctl stop 会将Erlang 虚拟机关闭,rabbitmqctl stop_app 只关闭 RabbitMQ 服务) 
rabbitmqctl reset 
rabbitmqctl join_cluster rabbit@node1 
rabbitmqctl start_app(只启动应用服务)

6、查看集群状态

rabbitmqctl cluster_status

在任意一台服务器上登录查看,即可看到集群信息。

删除集群节点

rabbitmqctl stop_app 
rabbitmqctl reset 
rabbitmqctl start_app 
rabbitmqctl cluster_status 
rabbitmqctl forget_cluster_node rabbit@node2(node1 机器上执行)

10.2镜像队列

10.2.1 使用原因

引入镜像队列(Mirror Queue)的机制,可以将队列镜像到集群中的其他 Broker 节点之上,如果集群中的一个节点失效了,队列能自动地切换到镜像中的另一个节点上以保证服务的可用性。

10.2.2 搭建步骤

1.启动三台集群节点

2.随便找一个节点添加 policy

3.在 node1 上创建一个队列发送一条消息,队列存在镜像队列

4.停掉 node1 之后发现 node2 成为镜像队列

5.就算整个集群只剩下一台机器了 依然能消费队列里面的消息

说明队列里面的消息被镜像队列传递到相应机器里面了

10.3 高可用负载均衡

10.3.1 整体架构图

10.3.2 搭建步骤

HAProxy 提供高可用性、负载均衡及基于TCPHTTP 应用的代理,支持虚拟主机,它是免费、快速并 且可靠的一种解决方案,包括 Twitter,Reddit,StackOverflow,GitHub 在内的多家知名互联网公司在使用。 HAProxy 实现了一种事件驱动、单一进程模型,此模型支持非常大的井发连接数。

扩展 nginx,lvs,haproxy 之间的区别: (总结)Nginx/LVS/HAProxy负载均衡软件的优缺点详解

10.4 Federation Exchange

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

有一个在北京的业务(Client 北京) 需要连接(broker 北京),向其中的交换器 exchangeA 发送消息,此时的网络延迟很小, (Client 北京)可以迅速将消息发送至 exchangeA 中,就算在开启了 publisherconfirm 机制或者事务机制的情 况下,也可以迅速收到确认信息。

此时又有个在深圳的业务(Client 深圳)需要向 exchangeA 发送消息, 那么(Client 深圳) (broker 北京)之间有很大的网络延迟,(Client 深圳) 将发送消息至 exchangeA 会经历一定 的延迟,尤其是在开启了 publisherconfirm 机制或者事务机制的情况下,(Client 深圳) 会等待很长的延迟 时间来接收(broker 北京)的确认信息,进而必然造成这条发送线程的性能降低,甚至造成一定程度上的阻塞。

将业务(Client 深圳)部署到北京的机房可以解决这个问题,但是如果(Client 深圳)调用的另些服务都部署在深圳,那么又会引发新的时延问题,总不见得将所有业务全部部署在一个机房,那么容灾又何以实现? 这里 使用 Federation 插件就可以很好地解决这个问题.

1.需要保证每台节点单独运行

2.在每台机器上开启 federation 相关插件

rabbitmq-plugins enable rabbitmq_federation 
rabbitmq-plugins enable rabbitmq_federation_management

3.原理图(先运行 consumer 在 node2 创建 fed_exchange)

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

5.添加 policy

6.成功

10.5 Federation Queue

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

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

1.原理图

2.添加 upstream(同上)

3.添加 policy

10.6 Shovel

10.6.1 使用原因

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

10.6.2 搭建步骤

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

rabbitmq-plugins enable rabbitmq_shovel 
rabbitmq-plugins enable rabbitmq_shovel_management

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

3.添加 shovel 源和目的地

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值