How to use the code to realize the automatic cancellation of the order without payment within 30 minutes?

Table of contents

  • Understand the needs
  • Scenario 1: Database Polling
  • Solution 2: JDK's delay queue
  • Solution 3: Time Wheel Algorithm
  • Solution 4: redis cache
  • Scenario 5: Using message queues

Understand the needs

In development, we often encounter some requirements for delayed tasks.

For example

  • If the order is not paid within 30 minutes, it will be automatically canceled
  • Send a text message to the user 60 seconds after the order is generated

For the above tasks, we give a professional name to describe them, that is delayed tasks. Then there will be a question here, what is the difference between this delayed task and a scheduled task? There are a total of the following differences

Scheduled tasks have a clear trigger time, delayed tasks do not

Timing tasks have an execution cycle, while delayed tasks are executed within a period of time after an event is triggered, without an execution cycle

Timing tasks generally execute batch operations that are multiple tasks, while delayed tasks are generally a single task

Next, we take judging whether the order is timed out as an example to analyze the solution

Scenario 1: Database Polling

train of thought

This solution is usually used in small projects, that is, to scan the database regularly through a thread, judge whether there is an overtime order by the order time, and then perform operations such as update or delete

accomplish

Quartz can be used to achieve, a brief introduction

The maven project introduces a dependency as shown below

xml

copy code

<dependency>   <groupId>org.quartz-scheduler</groupId>   <artifactId>quartz</artifactId>   <version>2.2.2</version> </dependency>

Call the Demo class MyJob as shown below

java

copy code

package com.rjzheng.delay1; ​ import org.quartz.*; import org.quartz.impl.StdSchedulerFactory; ​ public class MyJob implements Job { ​   public void execute(JobExecutionContext context) throws JobExecutionException {       System.out.println("要去数据库扫描啦。。。");   } ​   public static void main(String[] args) throws Exception {       // 创建任务       JobDetail jobDetail = JobBuilder.newJob(MyJob.class)               .withIdentity("job1", "group1").build();       // 创建触发器 每3秒钟执行一次       Trigger trigger = TriggerBuilder               .newTrigger()               .withIdentity("trigger1", "group3")               .withSchedule(                       SimpleScheduleBuilder                               .simpleSchedule()                               .withIntervalInSeconds(3).                               repeatForever())               .build();       Scheduler scheduler = new StdSchedulerFactory().getScheduler();       // 将任务及其触发器放入调度器       scheduler.scheduleJob(jobDetail, trigger);       // 调度器开始调度任务       scheduler.start();   } ​ }

Run the code, you can find that every 3 seconds, the output is as follows

copy code

要去数据库扫描啦。。。

advantage

Simple and easy to implement, supports cluster operation

shortcoming

  • Consumes a lot of memory on the server
  • There is a delay, for example, if you scan every 3 minutes, the worst delay is 3 minutes
  • Suppose you have tens of millions of orders, scanning like this every few minutes, the database loss is huge

Solution 2: JDK's delay queue

train of thought

This solution is implemented by using the DelayQueue that comes with JDK. This is an unbounded blocking queue. The queue can only get elements from it when the delay expires. Objects placed in the DelayQueue must implement the Delayed interface.

The DelayedQueue implementation workflow is shown in the figure below

Among them, Poll(): Get and remove the timeout element of the queue, if not, return empty

take(): Obtain and remove the timeout element of the queue, if not, wait the current thread until an element meets the timeout condition, and return the result.

accomplish

Define a class OrderDelay to implement Delayed, the code is as follows

java

copy code

package com.rjzheng.delay2; ​ import java.util.concurrent.Delayed; import java.util.concurrent.TimeUnit; ​ public class OrderDelay implements Delayed { ​   private String orderId; ​   private long timeout; ​   OrderDelay(String orderId, long timeout) {       this.orderId = orderId;       this.timeout = timeout + System.nanoTime();   } ​   public int compareTo(Delayed other) {       if (other == this) {           return 0;       }       OrderDelay t = (OrderDelay) other;       long d = (getDelay(TimeUnit.NANOSECONDS) - t.getDelay(TimeUnit.NANOSECONDS));       return (d == 0) ? 0 : ((d < 0) ? -1 : 1);   } ​   // 返回距离你自定义的超时时间还有多少   public long getDelay(TimeUnit unit) {       return unit.convert(timeout - System.nanoTime(), TimeUnit.NANOSECONDS);   } ​   void print() {       System.out.println(orderId + "编号的订单要删除啦。。。。");   } ​ }

The running test Demo is, we set the delay time to 3 seconds

java

copy code

package com.rjzheng.delay2; ​ import java.util.ArrayList; import java.util.List; import java.util.concurrent.DelayQueue; import java.util.concurrent.TimeUnit; ​ public class DelayQueueDemo { ​   public static void main(String[] args) {       List<String> list = new ArrayList<String>();       list.add("00000001");       list.add("00000002");       list.add("00000003");       list.add("00000004");       list.add("00000005"); ​       DelayQueue<OrderDelay> queue = newDelayQueue < OrderDelay > ();       long start = System.currentTimeMillis();       for (int i = 0; i < 5; i++) {           //延迟三秒取出           queue.put(new OrderDelay(list.get(i), TimeUnit.NANOSECONDS.convert(3, TimeUnit.SECONDS)));           try {               queue.take().print();               System.out.println("After " + (System.currentTimeMillis() - start) + " MilliSeconds");           } catch (InterruptedException e) {               e.printStackTrace();           }       }   } ​ }

The output is as follows

yaml

copy code

00000001编号的订单要删除啦。。。。 After 3003 MilliSeconds 00000002编号的订单要删除啦。。。。 After 6006 MilliSeconds 00000003编号的订单要删除啦。。。。 After 9006 MilliSeconds 00000004编号的订单要删除啦。。。。 After 12008 MilliSeconds 00000005编号的订单要删除啦。。。。 After 15009 MilliSeconds

It can be seen that there is a delay of 3 seconds and the order is deleted

advantage

High efficiency and low task trigger time delay.

shortcoming

  • After the server restarts, all data disappears, fearing downtime
  • Cluster expansion is quite troublesome
  • Due to the limitation of memory conditions, for example, if there are too many orders that have not been paid, OOM exceptions will easily occur.
  • High code complexity

Solution 3: Time Wheel Algorithm

train of thought

First, a picture of the time wheel (this picture is everywhere)

The time wheel algorithm can be compared to a clock, as shown in the above figure, the arrow (pointer) rotates at a fixed frequency in a certain direction, and each beating is called a tick. It can be seen that the timing wheel has three important attribute parameters, ticksPerWheel (number of ticks in a round), tickDuration (duration of a tick) and timeUnit (time unit), for example, when ticksPerWheel=60, tickDuration=1, timeUnit = second, this is completely similar to the constant movement of the second hand in reality.

 

If the current pointer is on 1 and I have a task that needs to be executed in 4 seconds, then the execution thread callback or message will be placed on 5. So what if it needs to be executed after 20 seconds, since the number of slots in this ring structure is only 8, if it takes 20 seconds, the pointer needs to rotate 2 more times. Position is above 5 after 2 laps (20 % 8 + 1)

accomplish

We use Netty's HashedWheelTimer to achieve

Add the following dependencies to Pom

xml

copy code

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

The test code HashedWheelTimerTest is as follows

java

copy code

package com.rjzheng.delay3; ​ import io.netty.util.HashedWheelTimer; import io.netty.util.Timeout; import io.netty.util.Timer; import io.netty.util.TimerTask; ​ import java.util.concurrent.TimeUnit; ​ public class HashedWheelTimerTest { ​   static class MyTimerTask implements TimerTask { ​       boolean flag; ​       public MyTimerTask(boolean flag) {           this.flag = flag;       } ​       public void run(Timeout timeout) throws Exception {           System.out.println("要去数据库删除订单了。。。。");           this.flag = false;       }   } ​   public static void main(String[] argv) {       MyTimerTask timerTask = new MyTimerTask(true);       Timer timer = new HashedWheelTimer();       timer.newTimeout(timerTask, 5, TimeUnit.SECONDS);       int i = 1;       while (timerTask.flag) {           try {               Thread.sleep(1000);           } catch (InterruptedException e) {               e.printStackTrace();           }           System.out.println(i + "秒过去了");           i++;       }   } ​ }

The output is as follows

copy code

1秒过去了 2秒过去了 3秒过去了 4秒过去了 5秒过去了 要去数据库删除订单了。。。。 6秒过去了

advantage

High efficiency, task trigger time delay time is lower than delayQueue, code complexity is lower than delayQueue.

shortcoming

  • After the server restarts, all data disappears, fearing downtime
  • Cluster expansion is quite troublesome
  • Due to the limitation of memory conditions, for example, if there are too many orders that have not been paid, OOM exceptions will easily occur.

Solution 4: redis cache

Idea one

Using redis's zset, zset is an ordered collection, each element (member) is associated with a score, and the value in the collection is obtained by sorting the score

Add element: ZADD key score member [ score member ...]

Query elements in order: ZRANGE key start stop [WITHSCORES]

Query element score: ZSCORE key member

Remove element: ZREM key member [member ...]

The test is as follows

bash

copy code

添加单个元素 redis> ZADD page_rank 10 google.com (integer) 1 ​ 添加多个元素 redis> ZADD page_rank 9 baidu.com 8 bing.com (integer) 2 ​ redis> ZRANGE page_rank 0 -1 WITHSCORES 1) "bing.com" 2) "8" 3) "baidu.com" 4) "9" 5) "google.com" 6) "10" ​ 查询元素的score值 redis> ZSCORE page_rank bing.com "8" ​ 移除单个元素 redis> ZREM page_rank google.com (integer) 1 ​ redis> ZRANGE page_rank 0 -1 WITHSCORES 1) "bing.com" 2) "8" 3) "baidu.com" 4) "9"

So how? We set the order timeout timestamp and order number as score and member respectively, and the system scans the first element to determine whether it has timed out, as shown in the figure below

achieve one

java

copy code

package com.rjzheng.delay4; ​ import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.Tuple; ​ import java.util.Calendar; import java.util.Set; ​ public class AppTest { ​   private static final String ADDR = "127.0.0.1"; ​   private static final int PORT = 6379; ​   private static JedisPool jedisPool = new JedisPool(ADDR, PORT); ​   public static Jedis getJedis() {       return jedisPool.getResource();   } ​   //生产者,生成5个订单放进去   public void productionDelayMessage() {       for (int i = 0; i < 5; i++) {           //延迟3秒           Calendar cal1 = Calendar.getInstance();           cal1.add(Calendar.SECOND, 3);           int second3later = (int) (cal1.getTimeInMillis() / 1000);           AppTest.getJedis().zadd("OrderId", second3later, "OID0000001" + i);           System.out.println(System.currentTimeMillis() + "ms:redis生成了一个订单任务:订单ID为" + "OID0000001" + i);       }   } ​   //消费者,取订单 ​   public void consumerDelayMessage() {       Jedis jedis = AppTest.getJedis();       while (true) {           Set<Tuple> items = jedis.zrangeWithScores("OrderId", 0, 1);           if (items == null || items.isEmpty()) {               System.out.println("当前没有等待的任务");               try {                   Thread.sleep(500);               } catch (InterruptedException e) {                   e.printStackTrace();               }               continue;           }           int score = (int) ((Tuple) items.toArray()[0]).getScore();           Calendar cal = Calendar.getInstance();           int nowSecond = (int) (cal.getTimeInMillis() / 1000);           if (nowSecond >= score) {               String orderId = ((Tuple) items.toArray()[0]).getElement();               jedis.zrem("OrderId", orderId);               System.out.println(System.currentTimeMillis() + "ms:redis消费了一个任务:消费的订单OrderId为" + orderId);           }       }   } ​   public static void main(String[] args) {       AppTest appTest = new AppTest();       appTest.productionDelayMessage();       appTest.consumerDelayMessage();   } ​ }

At this time, the corresponding output is as follows

It can be seen that the consumption order is almost always after 3 seconds.

However, there is a fatal flaw in this version. Under high concurrency conditions, multiple consumers will get the same order number. Let’s test the code on ThreadTest

java

copy code

package com.rjzheng.delay4; ​ import java.util.concurrent.CountDownLatch; ​ public class ThreadTest { ​   private static final int threadNum = 10;   private static CountDownLatch cdl = newCountDownLatch(threadNum); ​   static class DelayMessage implements Runnable {       public void run() {           try {               cdl.await();           } catch (InterruptedException e) {               e.printStackTrace();           }           AppTest appTest = new AppTest();           appTest.consumerDelayMessage();       }   } ​   public static void main(String[] args) {       AppTest appTest = new AppTest();       appTest.productionDelayMessage();       for (int i = 0; i < threadNum; i++) {           new Thread(new DelayMessage()).start();           cdl.countDown();       }   } ​ }

The output looks like this

 

Obviously, there is a situation where multiple threads consume the same resource.

solution

(1) Use distributed locks, but with distributed locks, the performance is degraded, and this solution will not be elaborated.

(2) Judge the return value of ZREM, and consume data only when it is greater than 0, so the consumerDelayMessage() method will

scss

copy code

if(nowSecond >= score){   String orderId = ((Tuple)items.toArray()[0]).getElement();   jedis.zrem("OrderId", orderId);   System.out.println(System.currentTimeMillis()+"ms:redis消费了一个任务:消费的订单OrderId为"+orderId); }

change into

erlang

copy code

if (nowSecond >= score) {   String orderId = ((Tuple) items.toArray()[0]).getElement();   Long num = jedis.zrem("OrderId", orderId);   if (num != null && num > 0) {       System.out.println(System.currentTimeMillis() + "ms:redis消费了一个任务:消费的订单OrderId为" + orderId);   } }

After this modification, re-run the ThreadTest class and find that the output is normal

Idea two

This solution uses Redis's Keyspace Notifications. The Chinese translation is the keyspace mechanism. This mechanism can provide a callback after the key fails. In fact, redis will send a message to the client. Redis version 2.8 or above is required.

achieve two

In redis.conf, add a configuration

notify-keyspace-events Ex

Run the code as follows

java

copy code

package com.rjzheng.delay5; ​ import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPubSub; ​ public class RedisTest { ​   private static final String ADDR = "127.0.0.1";   private static final int PORT = 6379;   private static JedisPool jedis = new JedisPool(ADDR, PORT);   private static RedisSub sub = new RedisSub(); ​   public static void init() {       new Thread(new Runnable() {           public void run() {               jedis.getResource().subscribe(sub, "__keyevent@0__:expired");           }       }).start();   } ​   public static void main(String[] args) throws InterruptedException {       init();       for (int i = 0; i < 10; i++) {           String orderId = "OID000000" + i;           jedis.getResource().setex(orderId, 3, orderId);           System.out.println(System.currentTimeMillis() + "ms:" + orderId + "订单生成");       }   } ​   static class RedisSub extends JedisPubSub {       @Override       public void onMessage(String channel, String message) {           System.out.println(System.currentTimeMillis() + "ms:" + message + "订单取消"); ​       }   } }

The output is as follows

 

It can be clearly seen that after 3 seconds, the order is cancelled.

ps: There is a flaw in the pub/sub mechanism of redis, the content of the official website is as follows

原:Because Redis Pub/Sub is fire and forget currently there is no way to use this feature if your application demands reliable notification of events, that is, if your Pub/Sub client disconnects, and reconnects later, all the events delivered during the time the client was disconnected are lost.

Flip: Redis's publish/subscribe is currently a fire and forget mode, so reliable notification of events cannot be achieved. That is to say, if the publish/subscribe client is disconnected and then reconnected, all events during the disconnection period of the client will be lost. Therefore, option 2 is not recommended. Of course, if you don't have high requirements for reliability, you can use it.

advantage

(1) Since Redis is used as the message channel, the messages are all stored in Redis. If the sending program or the task handler hangs up, after restarting, there is still the possibility of reprocessing the data.

(2) It is quite convenient to do cluster expansion

(3) High time accuracy

shortcoming

Additional redis maintenance is required

Scenario 5: Using message queues

train of thought

We can use the delay queue of rabbitMQ. RabbitMQ has the following two features, which can implement delay queues

RabbitMQ can set x-message-tt for Queue and Message to control the lifetime of the message. If it times out, the message becomes dead letter

The Queue of lRabbitMQ can be configured with two parameters x-dead-letter-exchange and x-dead-letter-routing-key (optional), which are used to control the occurrence of deadletter in the queue, and rerouting according to these two parameters. Combining the above two features, you can simulate the function of delaying messages. Specifically, I will write another article another day, and I will talk about it here, the space is too long.

advantage

高效,可以利用 rabbitmq 的分布式特性轻易的进行横向扩展,消息支持持久化增加了可靠性。

缺点

本身的易用度要依赖于 rabbitMq 的运维.因为要引用 rabbitMq,所以复杂度和成本变高。

Guess you like

Origin blog.csdn.net/qq_41221596/article/details/131628940