1. Analysis of pain points and difficulties
1.1 Core business scenarios
- E-commerce platform: The system automatically releases inventory and cancels the order if the user places an order.
- Shared Services: The user timed out after making an appointment and was not used, and the resources were automatically released and credit points were deducted.
- Financial transactions: During payment processing, if it is not confirmed after a certain period of time, the refund process will be automatically triggered
1.2 Technical Challenges
- High concurrent pressure: Large e-commerce platforms may generate tens of thousands of orders per second, and timed tasks need to be processed efficiently
- Data consistency: Order status changes need to maintain atomicity with related operations such as inventory, points, etc.
- Task Impotence: In a distributed environment, business exceptions caused by repeated execution of timing tasks should be prevented
- Performance loss: Full scanning of unpaid orders will put huge pressure on the database
- Delay tolerance: Maximum allowable deviation between task execution time and order creation time
2. Solution comparison and implementation
Solution 1: Database polling (timed scanning)
Core idea: Start the timing task, scan the database every once in a while, and find unpaid orders that have been created for more than 30 minutes to cancel.
Technology implementation:
import ; import ; import ; import ; import ; @Service public class OrderCancelService { @Autowired private OrderRepository orderRepository; @Autowired private InventoryService inventoryService; // Execute scan tasks every 5 minutes @Scheduled(fixedRate = 5 * 60 * 1000) @Transactional public void cancelOverdueOrders() { // Calculate the time point 30 minutes ago Date overdueTime = new Date(() - 30 * 60 * 1000); // Inquiry of all orders that have not been paid and have been created for more than 30 minutes List<Order> overdueOrders = ( , overdueTime); for (Order order : overdueOrders) { try { // Locking to prevent concurrent operations order = (()); // Check the order status again (optimistic lock) if (() == ) { // Release inventory ((), ()); // Update order status is Canceled (); (order); // Record operation log ("Order{}Timeout Cancel", ()); } } catch (Exception e) { // Record exception logs and perform compensation processing ("取消Order失败: {}", (), e); } } } }
Pros and cons:
advantage: Simple implementation without additional technology stack
-
shortcoming:
High pressure on the database (full scanning)
Low time accuracy (dependent on scan interval)
Unable to deal with massive data
Applicable scenarios: Systems with small order volume and low demands on timeliness
Solution 2: JDK Delay Queue (DelayQueue)
Core idea:Use the JDK-ownedDelayQueue
, set the delay time when putting an order into the queue, and the queue will automatically pop up elements after the delay time arrives.
Technology implementation:
import ; import ; import ; // Order delay object, implement Delayed interfaceclass OrderDelayItem implements Delayed { private final String orderId; private final long expireTime; // Expiry time (milliseconds) public OrderDelayItem(String orderId, long delayTime) { = orderId; = () + delayTime; } // Get the remaining delay time @Override public long getDelay(TimeUnit unit) { long diff = expireTime - (); return (diff, ); } // Compare element order, used for queue sorting @Override public int compareTo(Delayed other) { return (, ((OrderDelayItem) other).expireTime); } public String getOrderId() { return orderId; } } // Order delay processing service@Service public class OrderDelayService { private final DelayQueue<OrderDelayItem> delayQueue = new DelayQueue<>(); @Autowired private OrderService orderService; @PostConstruct public void init() { // Start the processing thread Thread processor = new Thread(() -> { while (!().isInterrupted()) { try { // Get expired order from the queue OrderDelayItem item = (); // Process timeout orders (()); } catch (InterruptedException e) { ().interrupt(); ("Delayed queue processing is interrupted", e); } catch (Exception e) { ("Processing timeout order failed", e); } } }); (true); (); } // Add order to delay queue public void addOrderToDelayQueue(String orderId, long delayTimeMillis) { (new OrderDelayItem(orderId, delayTimeMillis)); } }
Pros and cons:
-
advantage:
- Based on memory operation, high performance
- Simple implementation without additional components
-
shortcoming:
Distributed environments are not supported
Service restart will cause data loss
Memory pressure is high when the order volume is too large
Applicable scenarios: stand-alone environment, system with small order volume
Solution 3: Redis expired key monitoring
Core idea: Use Redis's expiration key listening mechanism to deposit the order ID into Redis as a key and set the 30-minute expiration time, which triggers the callback event when the key expires.
Technology implementation:
import ; import ; import ; import ; // Redis expired key listener@Component public class RedisKeyExpirationListener implements MessageListener { @Autowired private RedisTemplate<String, String> redisTemplate; @Autowired private OrderService orderService; // Listen to Redis's expired event channel @Override public void onMessage(Message message, byte[] pattern) { // Get expired Key (order ID) String orderId = (); // Check whether the order exists and is not paid if (("order_status:" + orderId)) { String status = ().get("order_status:" + orderId); if ("UNPAID".equals(status)) { // Perform the order cancellation operation (orderId); } } } } // Order service@Service public class OrderService { @Autowired private RedisTemplate<String, String> redisTemplate; // When creating an order, deposit the order ID into Redis and set the 30-minute expiration public void createOrder(Order order) { // Save order to database (order); // Save the order status to Redis and set the 30-minute expiration ().set( "order_status:" + (), "UNPAID", 30, ); } // When payment is successful, delete the key in Redis public void payOrder(String orderId) { // Update order status (orderId, ); // Delete the key in Redis to avoid triggering expired events ("order_status:" + orderId); } // Cancel the order public void cancelOrder(String orderId) { // Check order status Order order = (orderId).orElse(null); if (order != null && () == ) { // Release inventory and other operations ((), ()); // Update order status (); (order); } } }
Pros and cons:
-
advantage:
- Based on Redis high performance, it does not affect the main business process
- Natural support in distributed environments
-
shortcoming:
Redis is required
notify-keyspace-events
parameterExpiration event triggers delay (default 1 second)
A large number of keys expire at the same time may cause performance fluctuations
Applicable scenarios: Systems with medium order volume and need distributed support
Solution 4: RabbitMQ Delay Queue
Core idea: Using RabbitMQ's dead letter queue (DLX) feature, order messages are sent to a queue with TTL, and the messages are automatically forwarded to the processing queue after they expire.
Technology implementation:
import .*; import ; import ; import ; import ; import ; @Service public class OrderMQService { // Delay queue switch public static final String DELAY_EXCHANGE = ""; // Delay queue name public static final String DELAY_QUEUE = ""; // Dead letter switch public static final String DEAD_LETTER_EXCHANGE = ""; // Dead letter queue (actual processing queue) public static final String DEAD_LETTER_QUEUE = ""; // Routing key public static final String ROUTING_KEY = ""; @Autowired private RabbitTemplate rabbitTemplate; @Autowired private OrderService orderService; // Configure delay queues @Bean public DirectExchange delayExchange() { return new DirectExchange(DELAY_EXCHANGE); } // Configure dead letter queue @Bean public DirectExchange deadLetterExchange() { return new DirectExchange(DEAD_LETTER_EXCHANGE); } // Configure the delay queue and set the dead letter switch @Bean public Queue delayQueue() { Map<String, Object> args = new HashMap<>(); // Set up dead letter switch ("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE); // Set the dead message routing key ("x-dead-letter-routing-key", ROUTING_KEY); return new Queue(DELAY_QUEUE, true, false, false, args); } // Configure dead letter queue (actual processing queue) @Bean public Queue deadLetterQueue() { return new Queue(DEAD_LETTER_QUEUE, true); } // Bind the delay queue to the delay switch @Bean public Binding delayBinding() { return (delayQueue()).to(delayExchange()).with(ROUTING_KEY); } // Bind the dead letter queue to the dead letter switch @Bean public Binding deadLetterBinding() { return (deadLetterQueue()).to(deadLetterExchange()).with(ROUTING_KEY); } // Send order message to the delay queue public void sendOrderDelayMessage(String orderId, long delayTime) { (DELAY_EXCHANGE, ROUTING_KEY, orderId, message -> { // Set message TTL (milliseconds) ().setExpiration((delayTime)); return message; }); } // Consumption of dead letter queue messages (processing timeout orders) @RabbitListener(queues = DEAD_LETTER_QUEUE) public void handleExpiredOrder(String orderId) { try { // Process timeout orders (orderId); } catch (Exception e) { ("Failed to process timeout order: {}", orderId, e); // Retry mechanism or compensation logic can be added } } }
Pros and cons:
-
advantage:
- High message reliability (RabbitMQ persistence mechanism)
- Supports distributed environments
- High time accuracy (accurate to milliseconds)
-
shortcoming:
RabbitMQ middleware needs to be introduced
Complex configuration (involving switch and queue binding)
A large number of short-term TTL messages may affect performance
Applicable scenarios: Systems with large order volume and high demands on message reliability
Solution 5: Time-WheelTimer
Core idea: Drawing on Netty's time wheel algorithm, time is divided into multiple slots, each slot represents a time interval, the task is placed into the corresponding slot, and the task is executed when the time wheel rolls to the corresponding slot.
Technology implementation:
import ; import ; import ; import ; import ; // Order timeout processing service@Service public class OrderTimeoutService { // Create a time wheel, scroll every 100 milliseconds, processing up to 1024 slots private final Timer timer = new HashedWheelTimer(100, , 1024); @Autowired private OrderService orderService; // Add an order timeout task public void addOrderTimeoutTask(String orderId, long delayTimeMillis) { (new TimerTask() { @Override public void run(Timeout timeout) throws Exception { try { // Process timeout orders (orderId); } catch (Exception e) { ("Failed to process timeout order: {}", orderId, e); // You can add a retry mechanism if (!()) { ().newTimeout(this, 5, ); } } } }, delayTimeMillis, ); } // When the order is paid successfully, the timeout task will be canceled public void cancelTimeoutTask(String orderId) { // Implementation is simply required, and the mapping relationship between task ID and order ID is maintained. } }
Pros and cons:
-
advantage:
- Small memory usage (compared to DelayQueue)
- Efficient task scheduling (O (1) time complexity)
- Supports a large number of timed tasks
-
shortcoming:
Distributed environments are not supported
Service restart will cause tasks to be lost
Time accuracy depends on the tickDuration of the time round
Applicable scenarios: Systems with a stand-alone environment, a large order volume and high performance requirements
3. Solution comparison and selection suggestions
plan | advantage | shortcoming | Applicable scenarios |
---|---|---|---|
Database polling | Simple implementation | Poor performance and low time accuracy | Small order quantity and low timeliness requirements |
JDK Delay Queue | Simple implementation and high performance | Distributed or service restart data loss is not supported | Single machine, small order volume |
Redis expired key monitoring | Distributed support, good performance | Complex configuration and delay | Medium order volume, need distributed support |
RabbitMQ Delay Queue | High reliability and high time accuracy | Introduce middleware and complex configuration | Large order volume and high reliability requirements |
Time round algorithm | Small memory usage and high performance | Distributed or service restart lost | Single machine, extremely large order volume |
Recommended plan:
- Small and medium-sized systems: Solution 3 (Redis expired key monitoring), balancing performance and complexity
- Large distributed system: Solution 4 (RabbitMQ delay queue) to ensure reliability and scalability
- High-performance scenarios: Solution 5 (time round algorithm), suitable for processing massive orders on a single machine
4. Best Practice Suggestions
No matter which option you choose, the following points should be considered:
Idepotency design: Timed tasks need to ensure that the results of multiple executions are consistent
Exception handling: Add retry mechanism and compensation logic
Monitoring alarm: Monitor task execution and promptly detect failed orders
Performance optimization: Avoid full scanning and adopt batch processing
Downgrade strategy: The automatic cancel function is temporarily turned off during high concurrency and switched to manual processing
By rationally selecting technical solutions and doing detailed processing, it can not only meet business needs, but also ensure the stability and performance of the system.
The above are the five solutions for Java to automatically cancel the order if it is not paid and the detailed content of the comparison and analysis. For more information about Java order if it is not paid, please pay attention to my other related articles!