How to Implement Precise Order Auto‑Close in E‑Commerce: Timers, MQs, and Redis
This article explores five practical techniques for automatically closing unpaid e‑commerce orders—ranging from simple scheduled tasks to advanced RocketMQ delay queues, RabbitMQ dead‑letter queues, time‑wheel algorithms, and Redis expiration listeners—detailing their mechanisms, trade‑offs, and implementation code.
In e‑commerce and payment scenarios, users often place an order but abandon payment; the system must close the order after a specific time window with sub‑second accuracy, as seen on major platforms.
Scheduled task to close orders
RocketMQ delay queue
RabbitMQ dead‑letter queue
Time‑wheel algorithm
Redis expiration listener
1. Scheduled Task (Least Recommended)
Using a periodic scheduler to scan orders is inefficient because the scan interval creates a delay (e.g., a 10‑minute interval can cause up to a 10‑minute error) and generates unnecessary I/O load.
2. RocketMQ Delay Queue
RocketMQ supports delayed messages with predefined delay levels (1s, 5s, 10s, 30s, 1m, …, 2h). The producer sets the delay level, and the consumer receives the message after the configured delay.
Sending Delayed Message (Producer)
/**
* 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("Send delayed message result: {}", sendResult);
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("Send time: {}", format.format(new Date()));
return true;
} catch (Exception e) {
e.printStackTrace();
log.error("Delayed message push exception: {}, content: {}", e.getMessage(), body);
}
return false;
}Receiving Delayed Message (Consumer)
/**
* 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("Delay queue listener started successfully: " + topic);
} catch (MQClientException e) {
log.error("Delay queue listener start failed: {}", e.getErrorMessage());
System.exit(1);
}
});
}Although convenient, the built‑in delay levels are limited to 18 predefined intervals, making custom delays (e.g., 15 minutes) impossible without the commercial version.
3. RabbitMQ Dead‑Letter Queue
RabbitMQ lacks native delayed queues, but the same effect can be achieved using a dead‑letter exchange (DLX) and message TTL.
Dead‑Letter Exchange : When a message is rejected, expires, or the queue overflows, it is routed to the DLX.
Message TTL : Set per‑message expiration (e.g., 60000 ms). When the TTL expires, the message is dead‑lettered.
byte[] messageBodyBytes = "Hello, world!".getBytes();
AMQP.BasicProperties properties = new AMQP.BasicProperties();
properties.setExpiration("60000"); // 60 seconds
channel.basicPublish("my-exchange", "queue-key", properties, messageBodyBytes);Typical setup includes:
Creating a normal exchange (named delay)
Creating an auto‑expire queue (delay_queue1) with x-dead-letter-exchange pointing to delay Creating a processing queue (delay_queue2) bound to the same exchange to consume expired messages
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);Receiving messages simply involves listening on delay_queue2.
4. Time‑Wheel Algorithm
The time‑wheel uses a circular slot array (e.g., 3600 slots for one‑hour resolution). A timer advances the current index every second. Each slot holds a set of tasks with two attributes: the remaining cycle count and the order ID. When the index reaches a slot, tasks with a zero cycle are executed, others decrement their cycle count. This provides O(1) scheduling with second‑level precision.
5. Redis Expiration Listener
By enabling key‑space notifications ( notify-keyspace-events Ex) and implementing a KeyExpirationEventMessageListener, Redis can trigger order cancellation as soon as the order key expires.
Configuration
package com.zjt.shop.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
@Configuration
public class RedisListenerConfig {
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}Listener Implementation
package com.zjt.shop.common.util;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@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 set state to cancelled if still unpaid
// (actual DB code omitted for brevity)
log.info("Order {} timed out and was auto‑cancelled", orderNo);
}
} catch (Exception e) {
log.error("Error handling Redis expiration: {}", e.getMessage());
}
}
}Testing by inserting a Redis key with a 3‑second TTL shows the listener automatically updates the order status to cancelled.
These five methods illustrate different trade‑offs between simplicity, precision, and infrastructure requirements for implementing reliable order auto‑close logic.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java High-Performance Architecture
Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.
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.
