Summary of order timeout implementation scheme

Posted by please_explain on Sun, 06 Mar 2022 12:56:22 +0100

Summary of order timeout implementation scheme

Order timeout is a classic business scenario, which is very common in the mall system.

Common implementation schemes are as follows

  1. Timed polling
  2. Passive cancellation
  3. redis expiration callback
  4. Delay message

1, Timed polling

Implementation method: start a scheduled task, poll the database after a period of time, and close the overtime order.

advantage:

  1. The implementation method is simple

Disadvantages:

1. Poor timeliness is related to the polling time difference. The greater the polling time difference, the greater the order cancellation time error.
2. Low efficiency.
3. Great pressure on the database. If the polling interval is set to be small, the database needs to be read and written frequently.

2, Passive cancellation

Implementation method: when the user queries the information, we will judge whether it times out.

advantage:

1. Simple implementation
2. Compared with scheme I, it reduces the pressure on the database

Disadvantages:

  1. If the user does not query all the time, the order will not be cancelled, which will affect the inventory, order quantity, etc.

3, redis expiration callback

Redis's key expiration callback event can also achieve the effect of delaying the queue. In short, we enable the event of monitoring whether the key expires. Once the key expires, a callback event will be triggered.
Modify redis The conf file opens notify keyspace events ex.

Redis listening configuration, inject Bean RedisMessageListenerContainer.

@Configuration
public class RedisListenerConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {

    RedisMessageListenerContainer container = new RedisMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    return container;
	}
} 

To write Redis expiration callback listening method, you must inherit KeyExpirationEventMessageListener, which is a bit similar to MQ message listening.

@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {

    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }
    
    @Override
    public void onMessage(Message message, byte[] pattern) {
        String expiredKey = message.toString();
        System.out.println("Monitor key: " + expiredKey + "Expired ");
    }
} 	

At this point, the code is written. It's very simple. Next, test the effect. Add a key in the redis cli client and give the expiration time of 3s.

set xiaofu 123 ex 3

The expired key was successfully monitored on the console.

Monitoring expired key For: xiaofu

4, Delay queue

DelayQueue delay queue

JDK provides a set of API s to implement delay queue, which is located in Java util. DelayQueue under concurrent package.

DelayQueue is a BlockingQueue (unbounded blocking) queue. Its essence is to encapsulate a PriorityQueue (priority queue). The PriorityQueue uses a complete binary heap (unknown self-knowledge HA) to sort the queue elements. When adding elements to the DelayQueue queue, we will give the elements a Delay (Delay time) as the sorting condition, The smallest element in the queue takes precedence at the head of the queue. The elements in the queue can only be taken out of the queue after the Delay time. The basic data type or user-defined entity class can be placed in the queue. When storing the basic data type, the elements in the priority queue are arranged in ascending order by default. The user-defined entity class needs to be calculated according to the comparison of class attribute values.

First, simply implement it to see the effect. Add three order queue delayqueues and set the order to be cancelled after 5 seconds, 10 seconds and 15 seconds of the current time.

To implement the DelayQueue delay queue, the elements in the queue need the implements Delayed interface, which has only one getDelay method to set the delay time. The compareTo method in the Order class is responsible for sorting the elements in the queue.

public class Order implements Delayed {
/**
 * delay time 
 */
@JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
private long time;
String name;

public Order(String name, long time, TimeUnit unit) {
    this.name = name;
    this.time = System.currentTimeMillis() + (time > 0 ? unit.toMillis(time) : 0);
}

@Override
public long getDelay(TimeUnit unit) {
    return time - System.currentTimeMillis();
}
@Override
public int compareTo(Delayed o) {
    Order Order = (Order) o;
    long diff = this.time - Order.time;
    if (diff <= 0) {
        return -1;
    } else {
        return 1;
    }
}
} 

The put method of DelayQueue is thread safe because the put method uses the ReentrantLock lock internally for thread synchronization. DelayQueue also provides two methods to get out of the queue: poll() and take(). Poll() is a non blocking acquisition, and elements that do not expire directly return null; Take() is obtained by blocking, and the element thread that has not expired will wait.

public class DelayQueueDemo {

	public static void main(String[] args) throws InterruptedException {
    	Order Order1 = new Order("Order1", 5, TimeUnit.SECONDS);
    	Order Order2 = new Order("Order2", 10, TimeUnit.SECONDS);
    	Order Order3 = new Order("Order3", 15, TimeUnit.SECONDS);
    	DelayQueue<Order> delayQueue = new DelayQueue<>();
    	delayQueue.put(Order1);
    	delayQueue.put(Order2);
    	delayQueue.put(Order3);
    	System.out.println("Order delay queue start time:" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    	while (delayQueue.size() != 0) {
            /**
             * Get whether the queue header element is expired
             */
        	Order task = delayQueue.poll();
        	if (task != null) {
            	System.out.format("order:{%s}Cancelled, Cancel time:{%s}\n", task.name, 			LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        	}
        Thread.sleep(1000);
    	}
	}   
}

The above is just a simple operation of joining and leaving the team. In the actual development, there will be special threads responsible for joining and consuming messages.

After execution, you can see the following results: Order1, Order2 and Order3 are executed after 5 seconds, 10 seconds and 15 seconds respectively. So far, the delay queue is implemented with DelayQueue.

Order delay queue start time: 2020-05-06 14:59:09
 order:{Order1}Cancelled, Cancel time:{2020-05-06 14:59:14}
order:{Order2}Cancelled, Cancel time:{2020-05-06 14:59:19}
order:{Order3}Cancelled, Cancel time:{2020-05-06 14:59:24} 

Implementation of delay queue based on Redis

You can use the zset (sortedset) command, sort with the set timestamp as the score, and use the zadd score1 value1... Command to always produce messages into memory. Then use zrangebysocre to query all the qualified tasks to be processed, and execute the queue tasks in a circular way. You can also query the earliest task through zrangebyscore key min max WithCores limit 0 1 to consume.

The consumer polls the queue delayqueue and compares the minimum time after sorting the elements with the current time. If it is less than the current time, it means that the key has expired and removed.

/**
 * Consumption news
 */
public void pollOrderQueue() {

    while (true) {
        Set<Tuple> set = jedis.zrangeWithScores(DELAY_QUEUE, 0, 0);

        String value = ((Tuple) set.toArray()[0]).getElement();
        int score = (int) ((Tuple) set.toArray()[0]).getScore();

        Calendar cal = Calendar.getInstance();
        int nowSecond = (int) (cal.getTimeInMillis() / 1000);
        if (nowSecond >= score) {
            jedis.zrem(DELAY_QUEUE, value);
            System.out.println(sdf.format(new Date()) + " removed key:" + value);
        }

        if (jedis.zcard(DELAY_QUEUE) <= 0) {
            System.out.println(sdf.format(new Date()) + " zset empty ");
            return;
        }
        Thread.sleep(1000);
    }
} 

Implementation based on rabbitmq

Using RabbitMQ as a delay queue is a common way. In fact, RabbitMQ does not directly support the function of providing delay queue, but is indirectly realized through the TTL and DXL attributes of RabbitMQ message queue.

TTL, as its name suggests, refers to the Message survival time. RabbitMQ can set the Message survival time on the specified Queue and Message through the x-message-tt parameter. Its value is a non negative integer in microseconds.

Time To Live(TTL):

RabbitMQ can set the message expiration time from two dimensions: the queue and the message itself:

  • If the queue expiration time is set, all messages in the queue have the same expiration time.
  • Set the message expiration time. Set the expiration time for a message in the queue. The TTL of each message can be different.

Dead Letter Exchanges(DLX):

DLX is the Dead Letter switch, and the Dead Letter Queue bound to the Dead Letter switch. The Queue of RabbitMQ can be configured with two parameters: x-dead-letter-exchange and x-dead-letter-routing-key (optional). Once Dead Letter appears in the Queue, the message can be rerouted to another Exchange (switch) according to these two parameters, so that the message can be consumed again.

x-dead-letter-exchange: after Dead Letter appears in the queue, reroute and forward the Dead Letter to the specified exchange (switch).

x-dead-letter-routing-key: Specifies the routing key to send, which is generally the queue to be forwarded.

Dead Letter occurs in the queue:

  • The TTL of a message or queue has expired
  • The queue has reached its maximum length
  • The message is rejected by the consumer (basic.reject or basic.nack)

The following figure shows how to realize the function of unpaid customs clearance for more than 30 minutes. We send the order message A0001 to the delay queue order delay. Queue and set the x-message-tt message survival time to 30 minutes. After 30 minutes, the order message A0001 becomes Dead Letter, and the delay queue detects Dead Letter. By configuring x-dead-letter-exchange, retransmit the Dead Letter to the customs queue that can consume normally, and directly listen to the customs queue to process the customs logic.

When sending a message, specify the time to delay the message.

public void send(String delayTimes) {
        amqpTemplate.convertAndSend("order.pay.exchange", "order.pay.queue","Hello, I'm delayed data", message -> {
            // Set the delay value in milliseconds
            message.getMessageProperties().setExpiration(String.valueOf(delayTimes));
            return message;
        });
} 

Set forwarding rules after dead letter occurs in delay queue.

/**
 * Delay queue
 */
@Bean(name = "order.delay.queue")
public Queue getMessageQueue() {
    return QueueBuilder
            .durable(RabbitConstant.DEAD_LETTER_QUEUE)
            // Configure exchanges forwarded after expiration
            .withArgument("x-dead-letter-exchange", "order.close.exchange")
            // Configure routing keys for forwarding after expiration
            .withArgument("x-dead-letter-routing-key", "order.close.queue")
            .build();
} 

Topics: Docker Nginx Container