使用IDEA开发RabbitMQ教程系列(十)Spring Boot使用Spring AMQP的消息分发和限流

1、前言

上一篇文章我们介绍了Spring Boot整合Spring AMQP,也对消息confirm确认机制和Return消息机制进行了代码演练,今天我们来聊聊RabbitMQ的多个生产者和多个消费者之间的消费模式,在此前我们需要先来了解RabbitMQ的消息分发机制;

2、RabbitMQ的消息分发

当RabbitMQ某队列拥有多个消费者时,队列收到的消息将以轮询(round-robin )的分发方式发送给消费者。每条消息只会发送给订阅列表里的一个消费者,如何来理解这个轮询分发,我们用一张图来表示;
使用IDEA开发RabbitMQ教程系列(十)Spring Boot使用Spring AMQP的消息分发和限流_第1张图片
生产者将消息进行投递到交换机,交换机经过路由将消息转发至Queue队列,当多个消费者订阅了同一个队列的时候,队列将按照轮询来分发,得出上图消费者1 能获取到1-3-5-7-9 的消息,消费者2同理;

很多时候轮询的分发机制能满足我们大部分需求,但在吞吐量较大的应用,该机制还是有一定弊端,因为 RabbitMQ 不管消费者是否消费并己经确认(Basic.Ack) 了消息;试想一下,如果某些消费者任务繁重,来不及消费那么多的消息,而某些其他消费者由于某些原因(比如业务逻辑简单、机器性能卓越等)很快地处理完了所分配到的消息,进而进程空闲,这样就会造成整体应用吞吐量的下降

那么该如何处理这种情况呢?这里就要用到channel.basicQos(int prefetchCount)这个方法, channel.basicQos 方法允许限制信道上的消费者所能保持的最大未确认消息的数量;我们将使用Springboot来实现;

理论了这么多,是时候来一波代码演示了,还是继续我们上个集成项目来;

3、OneToMany 一个生产者多个消费者

生产者

@Component
public class One2ManySender implements RabbitTemplate.ReturnCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public void sendMsg() throws Exception {
        rabbitTemplate.setReturnCallback(this);
        for (int i = 0; i < 10; i++) {
            CorrelationData cd = new CorrelationData("123456789-" + i);
            rabbitTemplate.convertAndSend("one2manyExchange", "one2many.send", "测试发送" + i, cd);
            /**官方最新文档
            从版本2.1开始,该CorrelationData对象具有ListenableFuture您可以用来获取结果的对象,
            而不是ConfirmCallback在模板上使用
            **/
            boolean isAck = cd.getFuture().get(10, TimeUnit.SECONDS).isAck();
            //如果ACK true 进行业务处理
            if (isAck) {
                //获取发送消息时候的 CorrelationData
                System.err.println("通过cd.getFuture() 获取ID:" + cd.getId());
            } else {
                System.err.println("失败处理");
            }
        }
    }

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText,
                                String exchange, String routingKey) {
        System.err.println("--------------进入Return-------------");
        System.err.println("return exchange: " + exchange + ", routingKey: "
                + routingKey + ", replyCode: " + replyCode + ", replyText: " + replyText);
    }
}

两个消费者(复制多一个即可)

@Component
public class One2ManyReceiver1 {

    /**@Exchange 默认durable=true 故不需要设置 **
     *
     * @param message
     * @param channel
     * @throws Exception
     */
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "one2manyQueue",
                    durable="true"),
            exchange = @Exchange(value = "one2manyExchange",durable = "true",
                    type= "topic",
                    ignoreDeclarationExceptions = "true"),
            key = "one2many.*")
    )
    @RabbitHandler
    public void onMessage(Message message, Channel channel) throws Exception {
        System.err.println("RabbitReceiver1 消费端Payload: " + message.getPayload());
        Long deliveryTag = (Long)message.getHeaders().get(AmqpHeaders.DELIVERY_TAG);
        //手工ACK
        channel.basicAck(deliveryTag, false);
    }
}

最后再来一个controller 请求

@RestController
@RequestMapping("/send")
public class SenderController {

    @Resource
    private One2ManySender one2ManySender;
    
    @GetMapping("/one2many")
    @ResponseBody
    public String one2many() throws Exception {
        one2ManySender.sendMsg();
        return "one2many";
    }
 }

访问请求地址,最后输出如下效果:
使用IDEA开发RabbitMQ教程系列(十)Spring Boot使用Spring AMQP的消息分发和限流_第2张图片
从结果可以看出(附带了我们消息确认),消费者是平均接收了消息;证明了我们之前表述的队列收到的消息将以轮询(round-robin )的分发方式发送给消费者

4、ManyToMany 多个生产者多个消费者

还是一样,我们复制两个生产者 (Many2ManySender1、Many2ManySender2)和两个消费者(Many2ManyReceiver1 、Many2ManyReceiver2)
生产者:

@Component
public class Many2ManySender1 implements RabbitTemplate.ReturnCallback {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public void sendMsg() throws Exception {
        rabbitTemplate.setReturnCallback(this);
        for (int i = 0; i < 10; i++) {
            CorrelationData cd = new CorrelationData("987654321-" + i);
            rabbitTemplate.convertAndSend("many2manyExchange", "many2many.send", "测试发送" + i, cd);
            /**官方最新文档
            从版本2.1开始,该CorrelationData对象具有ListenableFuture您可以用来获取结果的对象,
            而不是ConfirmCallback在模板上使用
            **/
            boolean isAck = cd.getFuture().get(10, TimeUnit.SECONDS).isAck();
            //如果ACK true 进行业务处理
            if (isAck) {
                //获取发送消息时候的 CorrelationData
                System.err.println("通过cd.getFuture() 获取ID:" + cd.getId());
            } else {
                System.err.println("失败处理");
            }
        }
    }

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText,
                                String exchange, String routingKey) {
        System.err.println("--------------进入Return-------------");
        System.err.println("return exchange: " + exchange + ", routingKey: "
                + routingKey + ", replyCode: " + replyCode + ", replyText: " + replyText);
    }

}

消费者

@Component
public class Many2ManyReceiver1 {

    /**@Exchange 默认durable=true 故不需要设置 **
     *
     * @param message
     * @param channel
     * @throws Exception
     */
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "many2manyQueue",
                    durable="true"),
            exchange = @Exchange(value = "many2manyExchange",durable = "true",
                    type= "topic",
                    ignoreDeclarationExceptions = "true"),
            key = "many2many.*")
    )
    @RabbitHandler
    public void onMessage(Message message, Channel channel) throws Exception {
        System.err.println("Many2ManyReceiver1 消费端Payload: " + message.getPayload());
        Long deliveryTag = (Long)message.getHeaders().get(AmqpHeaders.DELIVERY_TAG);
        //手工ACK
        channel.basicAck(deliveryTag, false);
    }
}

添加一个controller 请求

    @GetMapping("/many2many")
    @ResponseBody
    public String many2many() throws Exception {
        many2ManySender1.sendMsg();
        many2ManySender2.sendMsg();
        return "many2many";
    }

我们运行项目,发现会有如下错误!Only one ReturnCallback is supported by each RabbitTemplate
使用IDEA开发RabbitMQ教程系列(十)Spring Boot使用Spring AMQP的消息分发和限流_第3张图片
出现这个问题是因为 RabbitTemplate只允许设置一个callback方法,而默认spring中维护的RabbitTemplate是单例的,所以导致多个生产者设置callback时候报错,我们可以通过将RabbitTemplate的作用域设为@Scope,每次bean都是新的,来解决这个问题!

    @Bean
    @Scope("prototype")
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate template = new RabbitTemplate(connectionFactory);
        return template;
    }

再次运行查看结果:
使用IDEA开发RabbitMQ教程系列(十)Spring Boot使用Spring AMQP的消息分发和限流_第4张图片

4、channel.basicQos消息限流

我们前面说到了RabbitMQ 不管消费者是否消费并己经确认(Basic.Ack) 了消息;试想一下,如果某些消费者任务繁重,来不及消费那么多的消息,而某些其他消费者由于某些原因(比如业务逻辑简单、机器性能卓越等)很快地处理完了所分配到的消息,进而进程空闲,这样就会造成整体应用吞吐量的下降
那么针对我们业务的需求和服务器的性能我们可以决定某些消费者,RabbitMQ使用channel.basicQos限制其信道上的消费者所能保持的最大未确认消息的数量;

那么springboot只需要添加一个配置,
使用IDEA开发RabbitMQ教程系列(十)Spring Boot使用Spring AMQP的消息分发和限流_第5张图片

5、结语

本文主要介绍了RabbitMQ消息分发的机制一对多、多对多的形式进行代码演示 以及消息的限流;大家可以参考演示代码自己敲一便加深印象,谢谢大家~

6、下期预告

下一篇:企业中常用的100%可靠性投递的解决方案

你可能感兴趣的:(RabbitMQ)