SoFunction
Updated on 2025-05-21

Java implements five solutions for automatically canceling orders without payment and comparative analysis

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 requirednotify-keyspace-eventsparameter

    • Expiration 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!