Backend Development 17 min read

Order Timeout Strategies: Scheduled Tasks, RocketMQ Delay Queue, RabbitMQ Dead‑Letter, Time Wheel, and Redis Expiration

The article compares five practical approaches for automatically closing unpaid orders—scheduled tasks, RocketMQ delayed messages, RabbitMQ dead‑letter queues, a time‑wheel algorithm, and Redis key‑expiration listeners—detailing their principles, advantages, limitations, and providing concrete Java code examples for each method.

Architect
Architect
Architect
Order Timeout Strategies: Scheduled Tasks, RocketMQ Delay Queue, RabbitMQ Dead‑Letter, Time Wheel, and Redis Expiration

Introduction

In e‑commerce and payment systems, orders that remain unpaid after a short period must be closed automatically; the article explores how to achieve precise order‑timeout handling (within seconds) using various backend techniques.

Common Approaches

Scheduled task closure

RocketMQ delayed queue

RabbitMQ dead‑letter queue

Time‑wheel algorithm

Redis expiration listener

1. Scheduled Task Closure (Least Recommended)

Using a cron job that runs every fixed interval (e.g., every 10 minutes) to scan and close orders can cause up to 10 minutes of delay and unnecessary I/O load, making it unsuitable for high‑precision requirements.

2. RocketMQ Delayed Queue

RocketMQ supports predefined delay levels (e.g., 1 s, 5 s, …, 2 h). A producer sets the delay level on the message, and the consumer processes it after the delay.

/**
 * Push delayed message
 * @param topic
 * @param body
 * @param producerGroup
 * @return boolean
 */
public boolean sendMessage(String topic, String body, String producerGroup) {
    try {
        Message recordMsg = new Message(topic, body.getBytes());
        producer.setProducerGroup(producerGroup);
        // set delay level 14 (10 minutes)
        recordMsg.setDelayTimeLevel(14);
        SendResult sendResult = producer.send(recordMsg);
        log.info("发送延迟消息结果:======sendResult:{}", sendResult);
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        log.info("发送时间:{}", format.format(new Date()));
        return true;
    } catch (Exception e) {
        e.printStackTrace();
        log.error("延迟消息队列推送消息异常:{},推送内容:{}", e.getMessage(), body);
    }
    return false;
}

The consumer registers a listener to receive the delayed messages:

/**
 * Receive delayed message
 * @param topic
 * @param consumerGroup
 * @param messageHandler
 */
public void messageListener(String topic, String consumerGroup, MessageListenerConcurrently messageHandler) {
    ThreadPoolUtil.execute(() -> {
        try {
            DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
            consumer.setConsumerGroup(consumerGroup);
            consumer.setVipChannelEnabled(false);
            consumer.setNamesrvAddr(address);
            consumer.subscribe(topic, "*");
            consumer.registerMessageListener(messageHandler);
            consumer.start();
            log.info("启动延迟消息队列监听成功:" + topic);
        } catch (MQClientException e) {
            log.error("启动延迟消息队列监听失败:{}", e.getErrorMessage());
            System.exit(1);
        }
    });
}

Limitation: only 18 fixed delay levels; custom delays require the commercial version.

3. RabbitMQ Dead‑Letter Queue

RabbitMQ lacks native delayed queues, so a dead‑letter exchange combined with message TTL is used. Messages are published with an expiration; when TTL expires, the message is routed to a dead‑letter exchange and then to a processing queue.

byte[] messageBodyBytes = "Hello, world!".getBytes();
AMQP.BasicProperties properties = new AMQP.BasicProperties();
properties.setExpiration("60000"); // 60 seconds
channel.basicPublish("my-exchange", "queue-key", properties, messageBodyBytes);

Configuration (Spring) creates the dead‑letter exchange, the delayed queue (with x‑dead‑letter‑exchange and x‑dead‑letter‑routing‑key), and the final processing queue:

@Configuration
public class DelayQueue {
    public static final String EXCHANGE = "delay";
    public static final String ROUTINGKEY2 = "delay_key";

    @Bean
    public DirectExchange defaultExchange() {
        return new DirectExchange(EXCHANGE, true, false);
    }

    @Bean
    public Queue queue() {
        return new Queue("delay_queue2", true);
    }

    @Bean
    @Autowired
    public Binding binding() {
        return BindingBuilder.bind(queue()).to(defaultExchange()).with(DelayQueue.ROUTINGKEY2);
    }

    @Bean
    @Autowired
    public SimpleMessageListenerContainer messageContainer2(ConnectionFactory connectionFactory) {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
        container.setQueues(queue());
        container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        container.setMessageListener((Message message, com.rabbitmq.client.Channel channel) -> {
            System.out.println("delay_queue2 收到消息 : " + new String(message.getBody()));
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        });
        return container;
    }
}

This method allows arbitrary delay times via TTL, but requires careful queue configuration.

4. Time‑Wheel Algorithm

A circular slot array (e.g., 3600 slots for one‑hour resolution) stores tasks. A timer moves a pointer each second; when a slot’s tasks have Cycle‑Num = 0 they are executed. This provides O(1) insertion and O(1) expiration checks, with second‑level precision.

5. Redis Expiration Listener

By enabling key‑space notifications (notify-keyspace-events Ex) and implementing a KeyExpirationEventMessageListener , the system can react to order‑key expiration and automatically cancel unpaid orders.

@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
    @Autowired
    private OrderInfoMapper orderInfoMapper;

    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        try {
            String key = message.toString();
            if (key != null && key.startsWith("order_")) {
                String orderNo = key.substring(6);
                QueryWrapper
queryWrapper = new QueryWrapper<>();
                queryWrapper.eq("order_no", orderNo);
                OrderInfoEntity orderInfo = orderInfoMapper.selectOne(queryWrapper);
                if (orderInfo != null && orderInfo.getOrderState() == 0) {
                    orderInfo.setOrderState(4);
                    orderInfoMapper.updateById(orderInfo);
                    log.info("订单号为【" + orderNo + "】超时未支付-自动修改为已取消状态");
                }
            }
        } catch (Exception e) {
            log.error("【修改支付订单过期状态异常】:" + e.getMessage());
        }
    }
}

After setting the Redis config and registering the listener, an order key with a 3‑second TTL will trigger automatic cancellation.

Conclusion

Each method has trade‑offs: scheduled tasks are simple but imprecise; RocketMQ offers limited fixed delays; RabbitMQ provides flexible TTL‑based delays; time‑wheel gives O(1) performance with second precision; Redis listeners enable immediate reaction without extra services. Choose the solution that best fits your latency, scalability, and infrastructure constraints.

BackendRedisMessage QueueRabbitMQrocketmqtime wheelorder timeout
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.