【RabbitMQ高级特性】延迟队列、事务和消息分发

 🔥个人主页: 中草药

🔥专栏:【中间件】企业级中间件剖析


一、延迟队列

ttl+私信队列实现延迟队列

Rabbitmq本事是没有直接支持延迟队列的功能,我们可以使用 ttl+私信队列 的方式来到达延迟队列的效果

        假设一个应用中需要将每条消息都设置为10秒的延迟,生产者通过 normal_exchange 这个交换器将发送的消息存储在 normal_queue 这个队列中,消费者订阅的并非是 normal_queue 这个队列,而是 dlx_queue 这个队列.当消息从 normal_queue 这个队列中过期之后被存入 dlx_queue 这个队列中,消费者就恰巧消费到了延迟10秒的这条消息.

生产者

@RestController
@RequestMapping("/producer")
public class ProducerController {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RabbitTemplate confirmRabbitTemplate;

    @RequestMapping("/dl")
    public String dl(){
        System.out.println("dl ...");
        //发送普通消息
        rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE,"normal","dl test");
        System.out.printf("%tc 消息发送成功 \n",new Date());
       
        return "dl test";
    }

}

消费者

package org.example.rabbitmqextensionsdemo.listener;

import com.rabbitmq.client.Channel;
import org.example.rabbitmqextensionsdemo.constant.Constants;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Date;

@Component
public class DLListener {
    @RabbitListener(queues = Constants.DL_QUEUE)
    public void dlHandleMessage(Message message, Channel channel) throws IOException {
        System.out.printf("[dl.queue] %tc 接收到消息: %s , deliveryTag = %d,\n",new Date(),new String(message.getBody(),"UTF-8"),
                message.getMessageProperties().getDeliveryTag());
    }

}

成功达到延迟队列的效果 

但是,使用 ttl——死信队列 存在一个问题

生产者

@RestController
@RequestMapping("/producer")
public class ProducerController {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RabbitTemplate confirmRabbitTemplate;

     @RequestMapping("/delay")
    public String delay(){
        System.out.println("delay ...");

        //lambda
        rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE,"normal","delay 30s test",message -> {
            message.getMessageProperties().setExpiration("30000");  //单位毫秒
            return message;
        });

        rabbitTemplate.convertAndSend(Constants.NORMAL_EXCHANGE,"normal","delay 10s test",message -> {
            message.getMessageProperties().setExpiration("10000");  //单位毫秒
            return message;
        });

        System.out.printf("%tc 消息发送成功 \n",new Date());
        return "delay test";
    }

}

消费者

@Component
public class DLListener {
    @RabbitListener(queues = Constants.DL_QUEUE)
    public void dlHandleMessage(Message message, Channel channel) throws IOException {
        System.out.printf("[dl.queue] %tc 接收到消息: %s , deliveryTag = %d,\n",new Date(),new String(message.getBody(),"UTF-8"),
                message.getMessageProperties().getDeliveryTag());
    }

}

此时

        这时会发现: 10s过期的消息,也是在30s后才进入到死信队列。

        消息过期之后,不一定会被马上丢弃,因为RabbitMQ只会检查队首消息是否过期,如果过期则丢到死信队列,此时就会造成一个问题,如果第一个消息的延时时间很长,第二个消息的延时时间很短,那第二个消息并不会优先得到执行.

        所以在考虑使用TTL+死信队列实现延迟任务队列的时候,需要确认业务上每个任务的延迟时间是一致的,如果遇到不同的任务类型需要不同的延迟的话,需要为每一种不同延迟时间的消息建立单独的消息队

延迟队列插件

Scheduling Messages with RabbitMQ | RabbitMQ

插件的安装使用都在上述文档,去到github上找到

Releases · rabbitmq/rabbitmq-delayed-message-exchange

合适的插件版本,安装至目录/usr/lib/rabbitmq/plugins 是⼀个附加目录, RabbitMQ包本⾝不会在此安装任何内容, 如果没有这个路径, 可以自己进行创建

#查看插件列表
rabbitmq-plugins list

# 启动插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange

# 重启服务
service rabbitmq-server restart

在安装并启动插件之后,我们可以在管理界面看到

此时可以使用插件提供的延迟队列

config

@Configuration
public class DelayConfig {
    @Bean("delayQueue")
    public Queue delayQueue() {
        return QueueBuilder.durable(Constants.DELAY_QUEUE).build();
    }

    @Bean("delayExchange")
    public DirectExchange delayExchange() {
        return ExchangeBuilder.directExchange(Constants.DELAY_EXCHANGE).delayed().build();
    }

    @Bean("delayBinding")
    public Binding delayBinding(@Qualifier("delayQueue") Queue queue, @Qualifier("delayExchange") DirectExchange delayExchange){
        return BindingBuilder.bind(queue).to(delayExchange).with("delay");
    }
}

生产者

@RestController
@RequestMapping("/producer")
public class ProducerController {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RabbitTemplate confirmRabbitTemplate;

    @RequestMapping("/delay2")
    public String delay2(){
        System.out.println("delay2 ...");

        //lambda
        rabbitTemplate.convertAndSend(Constants.DELAY_EXCHANGE,"delay","delay 30s test",message -> {
            message.getMessageProperties().setDelayLong(30000l);  //单位毫秒
            return message;
        });

        rabbitTemplate.convertAndSend(Constants.DELAY_EXCHANGE,"delay","delay 10s test",message -> {
            message.getMessageProperties().setDelayLong(10000l);  //单位毫秒
            return message;
        });

        System.out.printf("%tc 消息发送成功 \n",new Date());
        return "delay2 test";
    }
}

消费者

@Component
public class DelayListener {
    @RabbitListener(queues = Constants.DELAY_QUEUE)
    public void delayHandleMessage(Message message, Channel channel) throws IOException {
        System.out.printf("[delay.queue] %tc 接收到消息: %s ,\n",new Date(),new String(message.getBody(),"UTF-8"));
    }

}

此时结果,完美到达延迟队列的需求

常见面试题

介绍下 RabbitMQ 的延迟队列

        延迟队列是一个特殊的队列,消息发送之后,并不立即给消费者,而是等待特定的时间,才发送给消费者。延迟队列的应用场景有很多,比如:

  1. 订单在十分钟内未支付自动取消
  2. 用户注册成功后,3 天后发调查问卷
  3. 用户发起退款,24 小时后商家未处理,则默认同意,自动退款

但 RabbitMQ 本身并没直接实现延迟队列,通常有两种方法:

  1. TTL + 死信队列组合的方式
  2. 使用官方提供的延迟插件实现延迟功能

二者对比:

1. 基于死信实现的延迟队列

a. 优点:灵活不需要额外的插件支持

b. 缺点:

        1)存在消息顺序问题        

        2)需要额外的逻辑来处理死信队列的消息,增加了系统的复杂性

2. 基于插件实现的延迟队列

a. 优点:

        1)通过插件可以直接创建延迟队列,简化延迟消息的实现。

        2)避免了 DLX 的时序问题(插件提供新交换器类型 x - delayed - message,消息投递后先存于 mnesia 数据库,到达可投递时间才投递到目标队列,避免了时序问题。)

b. 缺点:

        1)需要依赖特定的插件,有运维工作

        2)只适用特定版本(早期的版本并不支持该插件)


二、事务

RabbitMQ 的事务机制主要用于确保消息的原子性,即在多个操作中要么全部成功,要么全部回滚。它通过 AMQP 协议提供的事务功能实现,适用于生产者需要严格保证消息可靠发送的场景。

config

@Configuration
public class RabbitMQConfig {

    //事务
    @Bean("transQueue")
    public Queue transQueue() {
        return QueueBuilder.durable(Constants.TRANS_QUEUE).build(); //设置队列的ttl为20s
    }
}

RabbitTemplateConfig

@Configuration
public class RabbitTemplateConfig {
    @Bean(name = "rabbitTemplate")
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        return new RabbitTemplate(connectionFactory);
    }

    @Bean(name = "transRabbitTemplate")
    public RabbitTemplate transRabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
        rabbitTemplate.setChannelTransacted(true);
        return rabbitTemplate;
    }

    @Bean
    public RabbitTransactionManager rabbitTransactionManager(ConnectionFactory connectionFactory) {
        return new RabbitTransactionManager(connectionFactory);
    }

}

生产者

@RestController
@RequestMapping("/producer")
public class ProducerController {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RabbitTemplate confirmRabbitTemplate;

    @Transactional
    @RequestMapping("/trans")
    public String trans(){
        System.out.println("trans ...");
        rabbitTemplate.convertAndSend("",Constants.TRANS_QUEUE,"trans test");
        rabbitTemplate.setChannelTransacted(true);
        //int num=3/0;
        rabbitTemplate.convertAndSend("",Constants.TRANS_QUEUE,"trans test2");
        return "trans test";
    }
}

三、消息分发

        RabbitMQ在队列多消费者场景下,默认采用轮询分发机制,将消息依次分配给各订阅消费者,确保每条消息仅由单一消费者处理。该机制虽便于通过横向扩展消费者数量应对负载增长,但存在潜在效率问题:当消费者处理能力存在差异时,可能导致部分节点消息积压(处理慢的消费者)或资源闲置(处理快的消费者),进而影响系统整体吞吐量。

        为优化消息分发公平性与资源利用率,RabbitMQ提供服务质量(QoS)控制机制,通过channel.basicQos(int prefetchCount)方法设定消费者最大未确认消息数上限(即预取阈值)。

Channel channel = connection.createChannel();
// 设置单个消费者最多同时处理 10 条未确认的消息
channel.basicQos(10);
// 启动消费者,关闭自动确认(autoAck=false)
channel.basicConsume(queueName, false, new DefaultConsumer(channel) {
    @Override
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        // 处理消息...
        channel.basicAck(envelope.getDeliveryTag(), false);
    }
});

        例如,若设置prefetchCount=5,RabbitMQ将跟踪该消费者当前未确认的消息数量,达到阈值后暂停向其投递新消息,直至已处理消息经确认释放配额。此机制通过类似TCP/IP协议中“滑动窗口”的流量控制策略,动态平衡消费者负载,避免单节点过载,从而提升系统处理效率与稳定性。

限流

        RabbitMQ 的限流主要通过 QoS(Quality of Service)机制 实现,核心目标是控制消费者处理消息的速率,避免资源过载。

        控制消费端一次只拉取N个请求通过设置prefetchCount参数,同时也必须要设置消息应答方式为手动应答

prefetchCount:控制消费者从队列中预取(prefetch)消息的数量,以此来实现流控制和负载均衡

config

spring:
  application:
    name: rabbitmq-extensions-demo
  rabbitmq:
    addresses: amqp://admin:admin@43.143.210.244:5672/Test
    listener:
      simple:
        acknowledge-mode: manual  # 消息接收确认
        #acknowledge-mode: auto
        #acknowledge-mode: none
        retry:
          enabled: true #开启消费者失败重试
          initial-interval: 5000ms #初始失败等待时长为5s
          max-attempts: 5 #最大重试次数 包括自身消费的一次
        prefetch: 5
    publisher-confirm-type: correlated  #消息发送确认

生产者

@RestController
@RequestMapping("/producer")
public class ProducerController {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RabbitTemplate confirmRabbitTemplate;

   @RequestMapping("/qos")
    public String qos(){
        System.out.println("qos ...");
        for (int i = 0; i < 20; i++) {
            rabbitTemplate.convertAndSend(Constants.QOS_EXCHANGE,"qos","qos test^"+i);
        }
        return "qos test";
    }
}

消费者

@Component
public class QosListener {
    @RabbitListener(queues = Constants.QOS_QUEUE)
    public void delayHandleMessage(Message message, Channel channel) throws IOException {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try{
            System.out.printf("接收到消息: %s , deliveryTag = %d,\n",new String(message.getBody(),"UTF-8"),
                    message.getMessageProperties().getDeliveryTag());
//            System.out.println("业务逻辑处理完成");
//            //肯定确认
//            channel.basicAck(deliveryTag,false);
        }catch (Exception e) {
            channel.basicNack(deliveryTag,false,true);
        }
    }

}

此时不进行消息确认,则会出现结果

成功起到了限流的作用

负载均衡(非公平发放)

        我们也可以用此配置,来实现"负载均衡",在有两个消费者的情况下,一个消费者处理任务非常快,另一个非常慢,就会造成一个消费者会一直很忙,而另一个消费者很闲,这是因为 RabbitMQ 只是在消息进入队列时分派消息,它不考虑消费者未确认消息的数量.

        我们可以使用设置prefetch=1的方式,告诉 RabbitMQ 一次只给一个消费者一条消息,也就是说,在处理并确认前一条消息之前,不要向该消费者发送新消息,相反,它会将它分派给下一个不忙的消费者

config

spring:
  application:
    name: rabbitmq-extensions-demo
  rabbitmq:
    addresses: amqp://admin:admin@43.143.210.244:5672/Test
    listener:
      simple:
        acknowledge-mode: manual  # 消息接收确认
        #acknowledge-mode: auto
        #acknowledge-mode: none
        retry:
          enabled: true #开启消费者失败重试
          initial-interval: 5000ms #初始失败等待时长为5s
          max-attempts: 1 #最大重试次数 包括自身消费的一次
        prefetch: 5
    publisher-confirm-type: correlated  #消息发送确认

生产者

@RestController
@RequestMapping("/producer")
public class ProducerController {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private RabbitTemplate confirmRabbitTemplate;

   @RequestMapping("/qos")
    public String qos(){
        System.out.println("qos ...");
        for (int i = 0; i < 20; i++) {
            rabbitTemplate.convertAndSend(Constants.QOS_EXCHANGE,"qos","qos test^"+i);
        }
        return "qos test";
    }
}

消费者

@Component
public class QosListener {
    @RabbitListener(queues = Constants.QOS_QUEUE)
    public void qosHandleMessage(Message message, Channel channel) throws IOException {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try{
            System.out.printf("001接收到消息: %s , deliveryTag = %d,\n",new String(message.getBody(),"UTF-8"),
                    message.getMessageProperties().getDeliveryTag());
            //肯定确认
            //通过Thread.sleep模拟不同的消费能力
            Thread.sleep(2000);
            channel.basicAck(deliveryTag,false);
        }catch (Exception e) {
            channel.basicNack(deliveryTag,false,true);
        }
    }

    @RabbitListener(queues = Constants.QOS_QUEUE)
    public void qosHandleMessage2(Message message, Channel channel) throws IOException {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try{
            System.out.printf("002接收到消息: %s , deliveryTag = %d,\n",new String(message.getBody(),"UTF-8"),
                    message.getMessageProperties().getDeliveryTag());
            //肯定确认
            Thread.sleep(1000);
            channel.basicAck(deliveryTag,false);
        }catch (Exception e) {
            channel.basicNack(deliveryTag,false,true);
        }
    }

}

这种情况就实现了负载均衡 


抛弃时间的人,时间也抛弃他。——莎士比亚

🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀

以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐

  制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值