Solution to order timeout unpaid


In the previous article Third party payment interface design I left a question: Order timeout closed. This question is often asked in the interview. In this article, we will explain it.

Similar scenarios to order timeout closing include:

  • Taobao automatically confirms receipt;
  • Wechat red envelopes have not been checked for 24 hours and need to be returned late;
  • When didi makes an appointment to take a taxi, the system will automatically cancel if the driver doesn't answer the order in ten minutes.

In view of the above, at the target time, the system automatically triggers the task to be executed instead of the user, with a professional name: delayed task.

For this kind of demand, the first thing we think of is to use scheduled tasks to scan the qualified data in the database and update it.

However, there are differences between delayed tasks and scheduled tasks:

  1. Timed tasks have a fixed trigger time, while delayed tasks are not fixed. It depends on the trigger time of business events. (for example, cancel the order half an hour after the order is generated).
  2. The scheduled task is periodic, and the delayed task ends after it is triggered, which is generally one-time;
  3. Timed tasks generally deal with multiple tasks, and delayed tasks are generally one task.

Let's take a look at the implementation of timed tasks.

Timing task implementation

There are several ways to implement scheduled tasks:

  • JDK comes with Timer implementation
  • Quartz framework implementation
  • Tasks built in after spring 3.0
  • Distributed task scheduling: XXL job

The general logic is as follows:

Hypothetical order table: t_order(id,end_time,status);

Data scanning:

select id from t_order where end_time>=30 and status=Initial state;

Modification:

update t_order set status=end where id in (Overtime order id);

Note: if the number of overtime orders is large, you need to query by page.

The advantage of this way is that it is simple to implement and supports distributed / cluster environment.

Disadvantages:

  1. Constantly scan the database through polling. If the amount of data is large and the task execution interval is short, it will cause a certain pressure on the database;
  2. Interval granularity is not easy to set;
  3. There is a delay: if you set a scan every 5 minutes, the worst delay time is 5 minutes.

Passive cancellation

Passive cancellation is consistent with lazy loading. When the user queries the order, judge whether the order times out. If so, follow the timeout logic.

This method depends on the user's query operation. If the user does not query, the order will not be cancelled.

This method is simple to implement and does not need to add additional processing operations. The disadvantage is low timeliness, which affects the user experience.

Now there is also a common processing method, which is the combination of timed task + passive cancellation.

The above is about the solution of timed tasks. Let's talk about the common technical implementation of delayed tasks.

Delay queue for JDK

It is implemented through the DelayQueue class provided by JDK. DelayQueue is an unbounded blocking queue that supports delayed acquisition of elements.
The elements in the queue must implement the Delayed interface and override the getDelay(TimeUnit) and compareTo(Delayed) methods.

The element can only be removed from the queue when the delay expires. And the queue is orderly, and the delay expiration time of the elements placed at the head of the queue is the longest.

Code demonstration

Define the element class as the element of the queue:

public class MyDelayedTask implements Delayed {

    private String orderId;
    private long startTime;
    private long delayMillis;

    public MyDelayedTask(String orderId, long delayMillis) {
        this.orderId = orderId;
        this.startTime = System.currentTimeMillis();
        this.delayMillis = delayMillis;
    }

    /**
     * Get delay time
     *
     * @param unit
     * @return
     */
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert((startTime + delayMillis) - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    /**
     * Sort elements in the queue by
     *
     * @param o
     * @return
     */
    @Override
    public int compareTo(Delayed o) {
        return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
    }

    public void exec() {
        System.out.println(orderId + "Order number to be deleted!!!");
    }
}

Define a main method test:

public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();

        list.add("00000001");
        list.add("00000002");
        list.add("00000003");
        list.add("00000004");
        list.add("00000005");
        
        long start = System.currentTimeMillis();

        for (int i = 0; i < list.size(); i++) {
            //Delay 3s
            delayQueue.put(new MyDelayedTask(list.get(i), 3000));
            delayQueue.take().exec();

            System.out.println("After " + (System.currentTimeMillis() - start) + " MilliSeconds");
        }
    }

Result printing:

00000001 Order number to be deleted!!!
After 3004 MilliSeconds
00000002 Order number to be deleted!!!
After 6009 MilliSeconds
00000003 Order number to be deleted!!!
After 9012 MilliSeconds
00000004 Order number to be deleted!!!
After 12018 MilliSeconds
00000005 Order number to be deleted!!!
After 15020 MilliSeconds

Advantages: high efficiency and low task trigger time delay.
Disadvantages:

  • After the server restarts, all data disappears for fear of downtime
  • Cluster expansion is quite troublesome
  • Because it is an unbounded queue, if there are too many tasks, it is easy to have OOM exceptions
  • High code complexity

Time wheel algorithm

Time wheel is a scheduling model that can efficiently use thread resources for batch scheduling. Bind a large number of scheduled tasks to the same scheduler, and use this scheduler to manage, trigger and run all tasks. It can efficiently manage all kinds of delayed tasks, periodic tasks, notification tasks and so on.

However, the time accuracy of the time wheel scheduler may not be very high, and it may not be suitable for scheduling tasks with particularly high accuracy requirements. Because the accuracy of the time wheel algorithm depends on the minimum granularity of the "pointer" unit in the time period. For example, the grid of the time wheel jumps once a second, so the tasks with scheduling accuracy less than one second cannot be scheduled by the time wheel. Moreover, the time wheel algorithm does not do downtime backup, so it is impossible to resume task rescheduling after downtime.

Code demonstration

Dependency:

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.69.Final</version>
</dependency>

Demo:

public class HashedWheelTimerTest {
    private static final long start = System.currentTimeMillis();

    public static void main(String[] args) {

        // Initialize netty time wheel
        HashedWheelTimer timer = new HashedWheelTimer(1, // time interval
                TimeUnit.SECONDS,
                10); // Number of slots in the time wheel

        TimerTask task1 = new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                System.out.println("Already" + costTime() + " Seconds, task1 Start execution");
            }
        };

        TimerTask task2 = new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                System.out.println("Already" + costTime() + " Seconds, task2 Start execution");
            }
        };

        TimerTask task3 = new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                System.out.println("Already" + costTime() + " Seconds, task3 Start execution");
            }
        };

        // Add task to delay queue
        timer.newTimeout(task1, 0, TimeUnit.SECONDS);
        timer.newTimeout(task2, 3, TimeUnit.SECONDS);
        timer.newTimeout(task3, 15, TimeUnit.SECONDS);
    }

    private static Long costTime() {
        return (System.currentTimeMillis() - start) / 1000;
    }
}

Redis zset implements delayed tasks

Zset is an ordered set. In Zset structure, each element (member) will have a score, and then all elements are arranged according to the size of the score.

We set the order timeout timestamp and order number to score and member respectively. In other words, the records in the collection list are sorted by execution time. We only need to take the records less than the current time.

Code demonstration

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;

import java.time.LocalDateTime;
import java.util.Set;
import java.util.UUID;

@Configuration
public class RedisDelayDemo {
    @Autowired
    private RedisTemplate redisTemplate;

    public void setDelayTasks(long delayTime) {
        String orderId = UUID.randomUUID().toString();
        Boolean addResult = redisTemplate.opsForZSet().add("delayQueue", orderId, System.currentTimeMillis() + delayTime);
        if (addResult) {
            System.out.println("Task added successfully!" + orderId + ", Current time is" + LocalDateTime.now());
        }
    }

    /**
     * Listening for delayed messages
     */
    public void listenDelayLoop() {
        while (true) {
            // Get a point-to-point message
            Set<String> set = redisTemplate.opsForZSet().rangeByScore("delayQueue", 0, System.currentTimeMillis(), 0, 1);

            // If not, wait
            if (set.isEmpty()) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // Continue execution
                continue;
            }
            // Get the key of the specific message
            String it = set.iterator().next();
            // Delete succeeded
            if (redisTemplate.opsForZSet().remove("delayQueue", it) > 0) {
                // Get the task
                System.out.println("Message expiration" + it + ",Time is" + LocalDateTime.now());
            }
        }
    }
}

Test:

@RequestMapping("/delayTest")
public void delayTest() {
    delayDemo.setDelayTasks(5000L);
    delayDemo.listenDelayLoop();
}

Operation results:

Task added successfully! e99961a0-fc1d-43d4-a83e-8db5fb6b3273, The current time is 2021-10-24T12:06:59.037363700
 Message expiration e99961a0-fc1d-43d4-a83e-8db5fb6b3273,The time is 2021-10-24T12:07:04.097486

advantage:

  1. Convenient cluster expansion
  2. High time accuracy
  3. Don't worry about downtime

Disadvantages: additional redis maintenance is required
Under the condition of high concurrency, multiple consumers may get the same order number. This situation can be handled by adding a distributed lock, but the performance degrades seriously.

MQ delay message

We can implement it through MQ delay message, taking RocketMQ as an example.

Usually, messages will be consumed by consumers immediately after delivery. When delayed messages are delivered, they need to set a specified delay level (different delay levels correspond to different delay times), that is, they will not be consumed by consumers until a specific time interval. In this way, the pressure at the database level is transferred to MQ, and there is no need for a handwriting timer, It reduces the business complexity. At the same time, MQ has its own peak shaving function, which can well cope with the business peak.

Code demonstration

Dependency:

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>5.0.0-PREVIEW</version>
</dependency>

Producer demo:

@Component
public class ProducerSchedule {

    private DefaultMQProducer producer;

    @Value("${rocketmq.producer.producer-group}")
    private String producerGroup;

    @Value("${rocketmq.namesrv-addr}")
    private String nameSrvAddr;

    public ProducerSchedule() {

    }

    /**
     * Producer structure
     *
     * @PostConstruct This annotation is used to modify the execution order of Bean initialization of a non static void() method:
     * Constructor(Construction method) - > @ Autowired - > @ postconstruct (annotated method)
     */
    @PostConstruct
    public void defaultMQProducer() {
        if (Objects.isNull(this.producer)) {
            this.producer = new DefaultMQProducer(this.producerGroup);
            this.producer.setNamesrvAddr(this.nameSrvAddr);
        }

        try {
            this.producer.start();
            System.out.println("Producer start");
        } catch (MQClientException e) {
            e.printStackTrace();
        }
    }

    /**
     * News release
     *
     * @param topic
     * @param messageText
     * @return
     */
    public String send(String topic, String messageText) {
        Message message = new Message(topic, messageText.getBytes());

        /**
         * Delayed message level settings
         * messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
         */
        message.setDelayTimeLevel(4);

        SendResult result = null;
        try {
            result = this.producer.send(message);
            System.out.println("Return information:" + JSON.toJSONString(result));
        } catch (Exception e) {
            e.printStackTrace();
        }

        return result.getMsgId();
    }
}

Consumer demo:

@Component
public class ConsumerSchedule implements CommandLineRunner {

    @Value("${rocketmq.consumer.consumer-group}")
    private String consumerGroup;

    @Value("${rocketmq.namesrv-addr}")
    private String nameSrvAddr;

    @Value("${rocketmq.topic}")
    private String rocketmqTopic;

    public void messageListener() throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(this.consumerGroup);
        consumer.setNamesrvAddr(this.nameSrvAddr);

        /**
         * Subscribe to topics
         */
        consumer.subscribe(rocketmqTopic, "*");

        /**
         * Set the number of consumption messages
         */
        consumer.setConsumeMessageBatchMaxSize(1);

        /**
         * Register message listening
         */
        consumer.registerMessageListener((MessageListenerConcurrently) (messages, context) -> {
            for (Message message : messages) {
                System.out.println("Listening message:" + new String(message.getBody()));
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });

        consumer.start();
    }

    @Override
    public void run(String... args) throws Exception {
        this.messageListener();
    }
}

The method to set the message delay level is setDelayTimeLevel(). At present, RocketMQ does not support delay messages with any time interval, and only supports delay messages with specific levels.

Write at the end

Today is [2021-10-24]. I wish you a happy programmer's day and achieve your little goal as soon as possible!!!

If you want to see more original articles, please pay attention to my official account number "ShawnLux".

Posted by finkrattaz on Sat, 23 Oct 2021 20:17:27 -0700