基于Rabbit MQ的延迟队列实现订单超时自动关单

引言

最近工作中开始接触支付、订单等相关业务内容,想要良好的解决订单超时未支付自动关闭的问题。传统解决方法有两种:

  1. 被动触发。只有当用户或商户查询订单信息时,再判断该订单是否超时,如果超时再进行超时逻辑的处理。这种做法实现简单,但是这种做法会导致用户体验极差,打开订单时需要极多的处理判断,甚至会对库存、订单量的统计带来误差。
  2. 写同步定时任务,定时扫描数据库表中的数据。这种处理方式只是适用比较小而简单的项目,当业务规模扩展时,依旧会带来很多问题,比如:效率极低;订单关闭不及时,订单自动关闭的及时与否取决于设置的扫描时间窗口,如果你设置每分钟定时轮询一次,那么订单取消时间的最大误差就有一分钟。当然也可能更大,比如一分钟之内有大量数据,但是一分钟没处理完,那么下一分钟的就会顺延;增加数据库的压力等。

在查阅资料之后,我们的解决方案寄望于延时队列的处理方式上。
延时队列的这种实现方式,包含两个重要的数据结构:环形队列,例如可以创建一个包含 2400 个 slot 的环形队列(本质是个数组);任务集合,环上每一个 slot 是一个 Set。本质上,就是一个时间轮算法的一个实现。
图片来源自网络

时间轮算法可以类比于时钟,如上图箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个 tick。这样可以看出定时轮由个3个重要的属性参数,ticksPerWheel(一轮的 tick 数),tickDuration(一个 tick 的持续时间)以及 timeUnit(时间单位),例如当 ticksPerWheel=60,tickDuration=1,timeUnit =秒,这就和现实中的始终的秒针走动完全类似了。

如果当前指针指在 1 上面,我有一个任务需要 4 秒以后执行,那么这个执行的线程回调或者消息将会被放在 5 上。那如果需要在20秒之后执行怎么办,由于这个环形结构槽数只到 7,如果要 20 秒,指针需要多转2圈。位置是在2圈之后的 5 上面(20 % 7 + 1)。

目前有很多消息队列都支持,比如 RocketMQ,RabbitMQ 等,本文以RabbitMQ为例,介绍如何使用RabbitMQ进行延时队列的创建。

1. Docker 安装Rabbit MQ
docker pull rabbitmq:management
docker run -dit --name rabbitmq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=password -p 15672:15672 -p 5672:5672 rabbitmq:management
2. Rabbit MQ安装延迟队列插件

下载与安装的Rabbitmq匹配的插件

rabbitmq-delayed-message-exchange

上传到服务器的/root文件夹下,然后进行如下操作

docker cp /root/rabbitmq_delayed_message_exchange-3.8.0.ez rabbitmq:/plugins

docker exec -it rabbitmq /bin/bash

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

rabbitmq-plugins list

看到如下图所示即为安装成功

3. pom引入Rabbit MQ
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
4. 创建RabbitConfig配置类
@Configuration
@RequiredArgsConstructor
public class RabbitConfig {

    private final CachingConnectionFactory connectionFactory;

    private final SimpleRabbitListenerContainerFactoryConfigurer factoryConfigurer;
    
    /**
     * 多个消费者
     *
     * @return SimpleRabbitListenerContainerFactory
     */
    @Bean(name = "multiListenerContainer")
    public SimpleRabbitListenerContainerFactory multiListenerContainer() {
        SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
        factoryConfigurer.configure(factory, connectionFactory);
        factory.setMessageConverter(new Jackson2JsonMessageConverter());
        factory.setAcknowledgeMode(AcknowledgeMode.NONE);
        factory.setConcurrentConsumers(10);
        factory.setMaxConcurrentConsumers(15);
        factory.setPrefetchCount(10);
        return factory;
    }

    @Bean
    public Queue reserveDelayQueue() {
        return new Queue("order.delay.queue.name", true);
    }

    @Bean
    public CustomExchange reserveDelayExchange() {
        Map<String, Object> pros = new HashMap<>(2);
        pros.put("x-delayed-type", "topic");
        return new CustomExchange("order.delay.exchange.name", "x-delayed-message", true, false, pros);
    }

    @Bean
    public Binding reserveDelayBindingNotify(@Qualifier("reserveDelayQueue") Queue queue,
                                 @Qualifier("reserveDelayExchange") CustomExchange customExchange) {
        return BindingBuilder.bind(queue).to(customExchange).with("order.delay.routing.key.name").noargs();
    }


}
3. 创建生产者
@Slf4j
@Component
@RequiredArgsConstructor
public class CancelOrderProducer {

    private final RabbitTemplate rabbitTemplate;

    public void sendMsg(Long orderId, Integer delayTime){
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        // 设置基本交换机
        rabbitTemplate.setExchange("order.delay.exchange.name");
        // 设置基本路由
        rabbitTemplate.setRoutingKey("order.delay.routing.key.name");

        rabbitTemplate.convertAndSend(orderId, new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                MessageProperties messageProperties = message.getMessageProperties();
                messageProperties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                message.getMessageProperties().setDelay(delayTime);
                return message;
            }
        });

        log.info("用户下单支付超时-发送用户下单记录id的消息到延迟队列:orderId={}", orderId);
    }


}
4. 业务中引用
@Slf4j
@Service
@RequiredArgsConstructor
public class ReserveServiceImpl extends ServiceImpl<ReserveMapper, HaReserve> implements ReserveService {
    // 引入生产者
    private final CancelOrderProducer cancelOrderProducer;
    
    @Value("${reserve.release-time}")
    private int autoCloseTime;

    @Override
    public JSONObject doReserve(ReserveDTO reserveDTO) {
    	... ...
    	// 省略业务代码
    	cancelOrderProducer.sendMsg(reserve.getId(), autoCloseTime);
    	return null;
    }
}
5. 创建消费者接收超时消息

具体的业务处理写在此处,一定注意要消费后要手动提交ack!!!

@Slf4j
@Component
@RequiredArgsConstructor
public class CancelOrderConsumer {

    private final RemoteBasicService remoteBasicService;

    @RabbitHandler
    @RabbitListener(queues = "order.delay.queue.name", containerFactory = "multiListenerContainer")
    public void handle(@Payload String orderId, Channel channel, Message message) {
        try {
            //业务处理
            log.info("用户下单支付超时取消订单-取消订单id:orderId={}",orderId);
            handleService(orderId, channel, message);

        } catch (Exception e) {
            log.error("{}", e.getMessage(), e);
        }
    }

    public void handleService(String orderId, Channel channel, Message message) throws IOException, InterruptedException {

        // channel.basicAck(long,boolean); 确认收到消息,消息将被队列移除,false只确认当前consumer一个消息收到,true确认所有consumer获得的消息。
        // channel.basicNack(long,boolean,boolean); 确认否定消息,第一个boolean表示一个consumer还是所有,第二个boolean表示requeue是否重新回到队列,true重新入队。
        // channel.basicReject(long,boolean); 拒绝消息,requeue=false 表示不再重新入队,如果配置了死信队列则进入死信队列。

        // 具体处理的业务代码省略
        ... ...
}
参考文章
  1. 订单超时未支付自动关闭的几种实现方案
  2. Docker安装Rabbitmq及其延时队列插件
<think>好的,我现在要处理用户RabbitMQ阻塞队列的查询。首先,我需要明确用户的问题:他们想了解RabbitMQ中阻塞队列实现与使用场景。接下来,我需要回顾提供的引用资料,找出相的信息。 首先看引用[2],里面提到了通过命令行查看阻塞的消息队列情况,以及处理堆积消息的方法。这说明阻塞队列可能和消息堆积有,可能涉及消费者处理速度慢或队列设置问题。引用[5]提到队列在没有消费者时的保留时间和消息的独设置,这可能与消息过期成为死信有,但死信队列和阻塞队列是否相呢?需要进一步分析。 然后引用[1]提到了优先级队列,但用户的问题是于阻塞队列,可能需要区分这两个概念。引用[3]和[4]讨论了延迟队列和死信队列,这些可能和阻塞队列不同,但或许在某些场景下有交集,比如消息堆积导致延迟处理。 现在需要确定阻塞队列RabbitMQ中的定义。根据我的知识,RabbitMQ本身没有显式的“阻塞队列”类型,但队列可能因为某些原因进入阻塞状态,比如内存或磁盘超过限制,导致生产者流控。此外,当消费者处理速度跟不上生产者时,队列中的消息会堆积,这可能导致队列阻塞。 引用[2]提到处理堆积消息,可能是指手动干预的情况,比如通过命令行工具检查并处理。这说明阻塞队列的处理可能需要运维介入,调整消费者或清理消息。 接下来,使用场景方面,阻塞队列可能出现在高并发写入但消费能力不足的情况,例如突发流量、消费者故障或处理逻辑复杂耗时。应用场景包括异步任务处理、削峰填谷等,但需要确保有监控和应急措施。 在实现机制上,需要结合RabbitMQ的流控机制,比如当内存或磁盘达到阈值时,Broker会阻塞生产者。此外,队列的配置参数如最大长度(x-max-length)或溢出行为(x-overflow)可能影响是否触发阻塞。 用户可能需要了解如何配置这些参数,以及如何处理阻塞情况,比如增加消费者、调整资源或设置警报。引用中的命令行操作和插件管理(如延迟队列插件)可能提供一些思路,但需明确阻塞队列的具体解决方案。 最后,要确保回答结构清晰,分点说明实现机制、使用场景和注意事项,并引用提供的资料适当标注。需要验证所有信息是否正确,比如RabbitMQ的流控机制和队列配置参数是否存在,以及处理阻塞的方法是否有效。</think>### RabbitMQ阻塞队列实现与使用场景 #### 一、阻塞队列实现机制 1. **队列阻塞触发条件** RabbitMQ队列阻塞通常由以下原因触发: - **生产者流量控制**:当Broker内存或磁盘达到阈值时,RabbitMQ会主动阻塞生产者的连接,避免系统崩溃[^2]。 - **消息堆积超限**:若队列设置最大长度(`x-max-length`)或最大容量(`x-max-length-bytes`),超出限制后新消息可能被拒绝或丢弃旧消息(取决于`x-overflow`策略)[^5]。 - **消费者不足**:当消费者处理速度低于生产者发送速度时,消息会堆积,导致队列处于“阻塞”状态。 2. **配置参数示例** 通过声明队列时设置参数实现流量控制: ```python # 伪代码示例:设置队列最大长度为1000,溢出时丢弃旧消息 channel.queue_declare( queue="my_queue", arguments={ "x-max-length": 1000, "x-overflow": "drop-head" # 或 "reject-publish" } ) ``` 3. **监控与处理** - 使用命令行工具查看队列状态: ```bash rabbitmqctl list_queues -p [vhost] name messages messages_ready messages_unacknowledged ``` 通过观察`messages_ready`字段判断是否堆积[^2]。 - 运维处理:登录RabbitMQ容器手动清理消息或扩容消费者。 --- #### 二、使用场景 1. **异步任务削峰填谷** 在突发流量场景下(如秒杀活动),通过阻塞队列暂存请求,避免后端服务过载。消费者按固定速率处理消息,实现流量平滑。 2. **资源敏感型任务** 当系统资源(如数据库连接)紧张时,队列阻塞可作为缓冲,防止资源耗尽。例如:数据库维护期间,队列暂存数据同步任务。 3. **优先级任务调度** 结合优先级队列(`x-max-priority`),高优先级消息可插队处理,低优先级消息可能因堆积被阻塞[^1]。 --- #### 三、注意事项 1. **死信队列联动** 若消息因超时(TTL)或溢出被拒绝,可配置死信队列(`x-dead-letter-exchange`)捕获异常消息,避免数据丢失[^3]。 2. **延迟队列插件** 对于需定时触发的阻塞场景(如订单超时闭),可启用`rabbitmq_delayed_message_exchange`插件,实现延迟队列[^4]。 3. **监控告警** - 监控`messages_unacknowledged`字段,判断消费者是否卡住。 - 配置RabbitMQ管理平台的告警规则,及时通知运维处理堆积。 ---
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值