Implementing Automatic Order Closure in E‑commerce: Scheduled Tasks, RocketMQ Delay Queue, RabbitMQ DLQ, Time Wheel, and Redis Expiration Listener
The article examines five backend techniques for automatically closing unpaid e‑commerce orders—scheduled tasks, RocketMQ delayed messages, RabbitMQ dead‑letter queues, a time‑wheel algorithm, and Redis key‑expiration listeners—detailing their principles, advantages, drawbacks, and sample Java code implementations.
In e‑commerce platforms, orders that remain unpaid must be closed after a predefined interval; this article evaluates five backend approaches for achieving precise order auto‑closure and provides concrete Java implementations.
1. Scheduled Task Closure (Least Recommended)
Using a periodic scheduler to scan orders and close them can introduce delays up to the task interval (e.g., a 10‑minute scan may close an order 10 minutes late), causing unnecessary I/O and unacceptable latency.
2. RocketMQ Delayed Queue
Delayed Message Concept
Producers send messages that are held for a specified delay before consumption. RocketMQ supports fixed delay levels (1 s, 5 s, …, 2 h) but not arbitrary precision.
Producer Example
/**
* 推送延迟消息
* @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);
// 设置延迟级别,14 对应 10 分钟
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;
}Consumer Example
/**
* 接收延迟消息
* @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);
}
});
}RocketMQ’s fixed delay levels limit flexibility; for example, a 15‑minute timeout cannot be expressed directly.
3. RabbitMQ Dead‑Letter Queue
RabbitMQ lacks native delayed queues, so a dead‑letter exchange (DLX) combined with message TTL is used to achieve delayed processing.
Dead‑Letter Exchange
A message enters the DLX when it is rejected without requeue, expires, or the queue overflows.
Message TTL
Set the expiration (in milliseconds) on a message; after the TTL the broker routes it to the DLX.
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 steps (exchange, queues, bindings) are illustrated with diagrams in the original article.
Sending a Delayed Message
String msg = "hello word";
MessageProperties messageProperties = newMessageProperties();
messageProperties.setExpiration("6000");
messageProperties.setCorrelationId(UUID.randomUUID().toString().getBytes());
Message message = newMessage(msg.getBytes(), messageProperties);
rabbitTemplate.convertAndSend("delay", "delay", message);Note: the queue that holds the delayed message must not have a consumer; otherwise the message is consumed before expiration.
4. Time‑Wheel Algorithm
A circular slot array (e.g., 3600 slots for one hour) stores tasks; a timer advances the current index every second. Each slot holds a set of tasks with a cycle count indicating how many full rotations remain before execution.
High efficiency—no full‑order scans.
Each order is processed exactly once.
Second‑level precision (adjustable by timer frequency).
5. Redis Expiration Listener
Enable key‑space notifications (e.g., notify-keyspace-events Ex ) and implement a listener extending KeyExpirationEventMessageListener to react when an order key expires.
package com.zjt.shop.common.util;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
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);
// query order and cancel if still unpaid
// ... (omitted for brevity)
log.info("订单号为【" + orderNo + "】超时未支付-自动修改为已取消状态");
}
} catch (Exception e) {
e.printStackTrace();
log.error("【修改支付订单过期状态异常】:" + e.getMessage());
}
}
}After configuring Redis to emit expiration events, storing a key with a 3‑second TTL will trigger the listener and automatically cancel the corresponding order.
Conclusion
The five methods each have trade‑offs; developers should choose based on precision requirements, infrastructure constraints, and scalability considerations.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.