Three Proven Spring Boot Strategies to Auto‑Cancel Orders After 30 Minutes
This guide walks you through three practical Spring Boot solutions—database scheduled scans, message‑queue delayed queues, and Redis key‑expiration notifications—to automatically cancel unpaid orders after 30 minutes, complete with code samples, architecture diagrams, pros and cons, and best‑practice recommendations.
Why Automatic Order Cancellation Is Required
In e‑commerce, food‑delivery, hotel‑booking and ticketing systems an order that remains unpaid after a fixed timeout must be cancelled to release inventory, restore coupons and free reserved resources. The cancellation logic must never cancel an order that has already been paid.
Implementation Options
Option 1 – Database Scheduled Scan
A Spring @Scheduled task periodically queries the orders table for rows whose status is PENDING_PAYMENT and whose create_time is older than the timeout (e.g., 30 minutes). Matching orders are passed to the cancellation service.
@Component
public class OrderCancelScheduler {
@Autowired
private OrderService orderService;
@Scheduled(fixedRate = 300000) // every 5 minutes
public void cancelTimeoutOrders() {
List<Order> timeoutOrders = orderService.findTimeoutOrders(30);
for (Order order : timeoutOrders) {
try {
orderService.cancelOrder(order.getId(), "超时未支付");
} catch (Exception e) {
log.error("取消订单失败,订单ID: {}", order.getId(), e);
}
}
}
} public List<Order> findTimeoutOrders(int minutes) {
LocalDateTime threshold = LocalDateTime.now().minusMinutes(minutes);
return orderMapper.selectTimeoutOrders(OrderStatus.PENDING_PAYMENT, threshold);
} <select id="selectTimeoutOrders" resultType="Order">
SELECT * FROM orders
WHERE status = #{status}
AND create_time <= #{timeThreshold}
</select>Pros: Simple, few dependencies, suitable for small projects.
Cons: Cancellation delay is bounded by the scan interval; scanning large tables can create load.
Option 2 – Message‑Queue Delayed Queue (Recommended)
This approach uses a delayed‑message exchange (RabbitMQ example) to schedule a cancellation message exactly 30 minutes after the order is created. The message is persisted, survives restarts and is processed by a consumer that checks the payment status before cancelling.
@Configuration
public class RabbitMQConfig {
@Bean
public CustomExchange orderDelayExchange() {
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange("order.delay.exchange", "x-delayed-message", true, false, args);
}
@Bean
public Queue orderDelayQueue() {
return new Queue("order.delay.queue", true);
}
@Bean
public Binding orderDelayBinding() {
return BindingBuilder.bind(orderDelayQueue())
.to(orderDelayExchange())
.with("order.delay.routingKey")
.noargs();
}
} public void createOrder(Order order) {
orderMapper.insert(order);
// push a 30‑minute delayed message
sendDelay(order.getId(), 30 * 60 * 1000);
}
private void sendDelay(Long orderId, int delayMs) {
rabbitTemplate.convertAndSend(
"order.delay.exchange",
"order.delay.routingKey",
orderId,
msg -> {
msg.getMessageProperties().setDelay(delayMs);
return msg;
});
} @Component
@RabbitListener(queues = "order.delay.queue")
public class OrderDelayConsumer {
@Autowired
private OrderService orderService;
@RabbitHandler
public void handle(Long orderId) {
Order order = orderService.getOrderById(orderId);
if (order != null && order.getStatus() == OrderStatus.PENDING_PAYMENT) {
orderService.cancelOrder(orderId, "超时未支付");
}
}
}Advantages: Precise timing, no database scanning, persistent tasks survive restarts, high concurrency performance.
Disadvantages: Requires a message‑queue infrastructure and operational expertise.
Option 3 – Redis Key Expiration Notification
When an order is created a Redis key order:{id} with a TTL of 30 minutes is set. Redis key‑space notifications fire when the key expires; a listener receives the event and cancels the order.
notify-keyspace-events Ex @Configuration
public class RedisListenerConfig {
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer c = new RedisMessageListenerContainer();
c.setConnectionFactory(connectionFactory);
return c;
}
} @Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
@Autowired
private OrderService orderService;
private static final String ORDER_PREFIX = "order:";
public RedisKeyExpirationListener(RedisMessageListenerContainer c) {
super(c);
}
@Override
public void onMessage(Message message, byte[] pattern) {
String key = message.toString();
if (key.startsWith(ORDER_PREFIX)) {
Long orderId = Long.parseLong(key.substring(ORDER_PREFIX.length()));
orderService.cancelOrder(orderId, "超时未支付");
}
}
} public void createOrder(Order order) {
orderMapper.insert(order);
redisTemplate.opsForValue().set("order:" + order.getId(), order.getId(), Duration.ofMinutes(30));
}
// On successful payment, delete the key to avoid cancellation
redisTemplate.delete("order:" + orderId);Pros: Very lightweight, no MQ required, easy to implement.
Cons: Expiration events are not strongly consistent and may be lost; single‑instance Redis carries a higher risk of missed events.
Global Configuration Examples
public enum OrderStatus {
PENDING_PAYMENT,
PAID,
CANCELLED,
COMPLETED
} @SpringBootApplication
@EnableScheduling
@EnableRabbit
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}Comparison of the Three Schemes
Database Scan: Precision limited by scan interval; moderate system pressure; reliability moderate; implementation simple; suitable for small projects.
MQ Delayed Queue: Highest timing precision; no database load; highest reliability due to persistence; higher operational complexity; recommended for production e‑commerce.
Redis Expiration: Good precision for most cases; low system pressure; reliability lower because events may be lost; very simple to implement; fits cost‑sensitive or low‑traffic scenarios.
Critical Best Practices
Make the cancel order operation idempotent so repeated executions do not double‑deduct inventory.
Always verify the order's payment status before performing cancellation.
For MQ solutions enable message persistence and use manual acknowledgments to avoid loss.
Provide a fallback mechanism (e.g., periodic database scan) to handle MQ or Redis notification failures.
Prefer MQ‑based delayed queues for high‑concurrency traffic; Redis TTL is acceptable only for low‑traffic use cases.
Conclusion
All three approaches are technically viable. Small or cost‑sensitive services can adopt the Redis TTL or simple scheduled‑scan method. Medium to large e‑commerce platforms should use a delayed‑queue on RabbitMQ (or RocketMQ/Kafka) for precise, reliable cancellation. For ultra‑high‑traffic spikes, combine a robust MQ delayed mechanism with a fallback database scan to guarantee correctness.
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.
Ray's Galactic Tech
Practice together, never alone. We cover programming languages, development tools, learning methods, and pitfall notes. We simplify complex topics, guiding you from beginner to advanced. Weekly practical content—let's grow together!
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.
