当前位置:网站首页>If the order has not been paid for 30 minutes, it will be automatically cancelled. How can I achieve this?

If the order has not been paid for 30 minutes, it will be automatically cancelled. How can I achieve this?

2022-06-25 00:57:00 Lazy bug~

In development , We often encounter some requirements for delayed tasks . for example

Generate order 30 Minutes unpaid , Automatically cancel .
Generate order 60 Seconds later , Send text messages to users .

For the above tasks , We give a professional name to describe , That's the delayed task . Then there will be a problem , What is the difference between delayed tasks and scheduled tasks ? There are the following differences :

Timed tasks have a clear trigger time , Delayed tasks do not .
Scheduled tasks have execution cycles , The delayed task is executed within a period of time after an event is triggered , No execution cycle .
Scheduled tasks generally perform batch operations, which are multiple tasks , Delayed tasks are usually single tasks .

below , Let's take the case of judging whether the order times out , Carry out scheme analysis .
Scheme analysis

One 、 Database polling

Ideas
This scheme is usually used in small projects , That is to scan the database regularly through a thread , Judge whether there are overtime orders by order time , Then proceed update or delete Wait for the operation
Realization
Bloggers used... In the early days of that year quartz To achieve ( The internship ), A brief introduction
maven The project introduces a dependency as follows

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

call Demo class MyJob As shown below

public class MyJob implements Job {
    

    public void execute(JobExecutionContext context)

            throws JobExecutionException {
    

        System.out.println(" I'm going to scan the database ...");

    }

 

    public static void main(String[] args) throws Exception {
    

        //  Create tasks 

        JobDetail jobDetail = JobBuilder.newJob(MyJob.class)

                .withIdentity("job1", "group1").build();

        //  Create trigger   Every time 3 Once per second 

        Trigger trigger = TriggerBuilder

                .newTrigger()

                .withIdentity("trigger1", "group3")

                .withSchedule(

                 SimpleScheduleBuilder.simpleSchedule()
                .withIntervalInSeconds(3).repeatForever())

                .build();

        Scheduler scheduler = new StdSchedulerFactory().getScheduler();

        //  Put the task and its trigger into the scheduler 
        scheduler.scheduleJob(jobDetail, trigger);

        //  The scheduler starts scheduling tasks 
        scheduler.start();
    }
}

Run code , It can be found that every 3 second , Output is as follows

I'm going to scan the database ...

Advantages and disadvantages
advantage : It's easy , Support cluster operation
shortcoming :
(1) Large memory consumption on the server
(2) There is a delay , For example, every time you 3 Scan every minute , The worst delay time is 3 minute
(3) Suppose you have tens of millions of orders , Scan like this every few minutes , The database is extremely lossy

Two 、JDK The delay queue for

Ideas
The solution is to use JDK Self contained DelayQueue To achieve , This is an unbounded blocking queue , The queue can only get elements from it when the delay expires , Put in DelayQueue Objects in the , It is necessary to achieve Delayed Interface .
DelayedQueue The implementation workflow is shown in the figure below
 Insert picture description here
among Poll(): Get and remove the timeout element of the queue , If not, return null .
take(): Get and remove the timeout element of the queue , If not wait Current thread , Until an element satisfies the timeout condition , Return results .
Realization
Define a class OrderDelay Realization Delayed, The code is as follows

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);
    }

    //  Returns how much time is left before your custom timeout 
    public long getDelay(TimeUnit unit) {
    
        return unit.convert(timeout - System.nanoTime(),TimeUnit.NANOSECONDS);
    }

    void print() {
    
        System.out.println(orderId+" The order number is to be deleted ....");
    }
}

Running tests Demo by , We set the delay time to 3 second

public class DelayQueueDemo {
    

     public static void main(String[] args) {
      

            // TODO Auto-generated method stub 
            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++){
      
                // Three seconds later 
                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) {
      
                    // TODO Auto-generated catch block 
                    e.printStackTrace();  
                }  
            }  
        }  
}

Output is as follows

00000001 The order number is to be deleted ....
After 3003 MilliSeconds

00000002 The order number is to be deleted ....
After 6006 MilliSeconds

00000003 The order number is to be deleted ....
After 9006 MilliSeconds

00000004 The order number is to be deleted ....
After 12008 MilliSeconds

00000005 The order number is to be deleted ....
After 15009 MilliSeconds

You can see that it's all delays 3 second , The order was deleted
Advantages and disadvantages
advantage : Efficient , Low task trigger time delay .
shortcoming :(1) After the server restarts , The data is all gone , Fear of downtime
(2) Cluster expansion is quite troublesome
(3) Because of memory constraints , For example, there are too many unpaid orders , Then it's easy to OOM abnormal
(4) High code complexity

3、 ... and 、 Time wheel algorithm

Ideas
Let's start with a picture of the time wheel ( This picture is everywhere )
 Insert picture description here
The algorithm of time wheel can be compared with clock , As shown in the arrow above ( The pointer ) To rotate at a fixed frequency in a certain direction , Every beat is called a tick. It can be seen that the timing wheel consists of 3 An important attribute parameter ,ticksPerWheel( One round tick Count ),tickDuration( One tick Duration of ) as well as timeUnit( Time unit ), For example, when ticksPerWheel=60,tickDuration=1,timeUnit= second , This is exactly like the real second hand moving .

If the current pointer is at 1 above , I have a task to do 4 Seconds later , Then the executed thread callback or message will be placed in 5 On . Then if you need to be in 20 What to do in seconds , Because the number of grooves in this ring structure is only 8, If you want to 20 second , The pointer needs to rotate more 2 circle . The location is 2 After the circle 5 above (20 % 8 + 1)
Realization
We use it Netty Of HashedWheelTimer To achieve
to pom Add the following dependencies

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

Test code HashedWheelTimerTest As shown below

public class HashedWheelTimerTest {
    

    static class MyTimerTask implements TimerTask{
    
        boolean flag;
        public MyTimerTask(boolean flag){
    
            this.flag = flag;
        }

        public void run(Timeout timeout) throws Exception {
    

            // TODO Auto-generated method stub
             System.out.println(" I'm going to delete the order in the database ....");
             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) {
    

                // TODO Auto-generated catch block
                e.printStackTrace();
            }

            System.out.println(i+" Seconds passed ");

            i++;

        }
    }
}

Output is as follows

1 Seconds passed
2 Seconds passed
3 Seconds passed
4 Seconds passed
5 Seconds passed
I'm going to delete the order in the database ....
6 Seconds passed

Advantages and disadvantages
advantage : Efficient , The task trigger time is longer than the delay time delayQueue low , Code complexity ratio delayQueue low .
shortcoming :
(1) After the server restarts , The data is all gone , Fear of downtime
(2) Cluster expansion is quite troublesome
(3) Because of memory constraints , For example, there are too many unpaid orders , Then it's easy to OOM abnormal

Four 、redis cache

Ideas
utilize redis Of zset.zset Is an ordered set , Every element (member) It's all related to a score, adopt score Sort to get the values in the set .

Additive elements :ZADD key score member [[score member] [score member] …]
Query elements in order :ZRANGE key start stop [WITHSCORES]
Query element score:ZSCORE key member
Remove elements :ZREM key member [member …]

Test the following

# Add a single element
redis> ZADD page_rank 10 google.com
(integer) 1

# Add multiple elements
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”

# Of the query element score value
redis> ZSCORE page_rank bing.com
“8”

# Remove single element
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 to achieve it ? We set the order timeout time stamp and order number to score and member, The system scans the first element to see if it times out , The details are shown in the following figure
 Insert picture description here
Realization

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();

    }


    // producer , Generate 5 Put an order in 

    public void productionDelayMessage(){
    

        for(int i=0;i<5;i++){
    

            // Delay 3 second 
            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 An order task is generated : Order ID by "+"OID0000001"+i);

        }
    }

    // consumer , Take the order 
    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(" There are currently no tasks waiting ");
                try {
    
                    Thread.sleep(500);
                } catch (InterruptedException e) {
    
                    // TODO Auto-generated catch block
                    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 Consumed a task : Consumer orders OrderId by "+orderId);
            }
        }
    }

    public static void main(String[] args) {
    
        AppTest appTest =new AppTest();
        appTest.productionDelayMessage();
        appTest.consumerDelayMessage();
    }

}

At this time, the corresponding output is as follows
 Insert picture description here
You can see , Almost all 3 Seconds later , Consumption order .

However , This edition has a fatal hard injury , Under the condition of high concurrency , Multiple consumers will get the same order number , Let's test the code ThreadTest.

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) {
    

                // TODO Auto-generated catch block

                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 is as follows
 Insert picture description here
obviously , Multiple threads consume the same resource .

Solution
(1) Use distributed locks , But with distributed locks , Performance is down , The plan is not detailed .
(2) Yes ZREM The return value of , Only greater than 0 When , Consumption data
, So will consumerDelayMessage() In the method

if(nowSecond >= score){
    

    String orderId = ((Tuple)items.toArray()[0]).getElement();

    jedis.zrem("OrderId", orderId);

    System.out.println(System.currentTimeMillis()+"ms:redis Consumed a task : Consumer orders OrderId by "+orderId);
}

It is amended as follows

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 Consumed a task : Consumer orders OrderId by "+orderId);
    }
}

After this modification , Rerun ThreadTest class , The output is found to be normal

  • Train of thought two
    The scheme ** Use redis Of Keyspace Notifications, Chinese translation is the key space mechanism , This mechanism can be used in key After failure , Provide a callback , It's actually redis A message will be sent to the client .** Is the need to redis edition 2.8 above .
    Achieve two
    stay redis.conf in , Add a configuration
notify-keyspace-events Ex

The running code is as follows

package com.rjzheng.delay5;

import redis.clients.jedis.Jedis;
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, "[email protected]__: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+" Order generation ");
        }

    }
    
    static class RedisSub extends JedisPubSub {
    

        <ahref='http://www.jobbole.com/members/wx610506454'>@Override</a>

        public void onMessage(String channel, String message) {
    

            System.out.println(System.currentTimeMillis()+"ms:"+message+" Order cancellation ");
        }
    }
}

Output is as follows
 Insert picture description here
It can be seen clearly 3 Seconds later , The order was cancelled
ps:redis Of pub/sub There is a hard injury to the mechanism , The official website is as follows

primary :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.

double : Redis Release / Subscription is currently send and discard (fire and
forget) Mode , Therefore, reliable notification of events cannot be realized . in other words , If released / The subscribed client is disconnected and reconnected , Then all events during client disconnection are lost .
therefore , Scheme 2 is not very recommended . Of course , If you don't require high reliability , have access to .

Advantages and disadvantages
advantage :
(1) Due to the use Redis As a message channel , Messages are stored in Redis in . If the sender or task handler hangs , After reboot , And the possibility of reprocessing the data .
(2) Cluster expansion is quite convenient
(3) High time accuracy
shortcoming :(1) It needs to be extra redis maintain

5、 ... and 、 Using message queuing

We can use RabbitMQ Of Delay queue .RabbitMQ It has the following two characteristics , Delay queue can be realized .

RabbitMQ Can target Queue and Message Set up x-message-tt, To control the lifetime of messages , If the timeout , Then the message becomes dead letter.

lRabbitMQ Of Queue You can configure the x-dead-letter-exchange and x-dead-letter-routing-key( Optional ) Two parameters , Used to control the occurrence of deadletter, Then reroute according to these two parameters .
Combine the above two features , You can simulate the function of delayed messages , Concrete , I'll write another article another day , Let's go on here , Length is too long .

Advantages and disadvantages

advantage : Efficient , You can use rabbitmq The distributed nature of easily scales horizontally , Message persistence support increases reliability .

shortcoming : Its ease of use depends on rabbitMq Operation and maintenance of , Because you want to reference rabbitMq, So the complexity and cost become higher .

Link to the original text
https://blog.csdn.net/hjm4702192/article/details/80519010

原网站

版权声明
本文为[Lazy bug~]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/176/202206242009022181.html