訂單自動過時實現方案

需求分析:

24小時內未支付的訂單過時失效。

解決方案

  1. 被動設置:在查詢訂單的時候檢查是否過時並設置過時狀態。
  2. 定時調度:定時器定時查詢並過時須要過時的訂單。
  3. 延時隊列:將未支付的訂單放入一個延時隊列中,依次取出過時訂單。
  4. 過時提醒:reids支持將一個過時的key(訂單號)通知給客戶端,根據過時的訂單號進行相應的處理。

1. 被動設置

這個太簡單了,就是在查詢的時候判斷是否失效,若是失效了就給他設置失效狀態。可是弊端也很明顯,每次查詢都要對未失效的訂單作判斷,若是用戶不查詢,訂單就不失效,那麼若是有相似統計失效狀態個數的功能,將會受到影響,因此只能適用於簡單獨立的場景。簡直low爆了。java

2. 定時調度

這種是常見的方法,利用一個定時器,在設置的週期內輪詢檢查並處理須要過時的訂單。
具體實現有基於Timer的,有基於Quartz,還有springboot自帶的Scheduler,實現起來比較簡單。
就寫一下第三個的實現方法吧:redis

  1. 啓動類加上註解@EnableScheduling
  2. 新建一個定時調度類,方法上加上@Scheduled註解,以下圖那麼簡單。

image.png

弊端spring

  1. 不可以精準的去處理過時訂單,輪詢週期設置的越小,精準度越高,可是項目的壓力越大,咱們上一個項目就有這種情況,太多定時器在跑,項目運行起來比較笨重。
  2. 並且須要處理的是過時的訂單,可是要查詢全部未支付的訂單,範圍大。對於大訂單量的操做不合適。

3. 延時隊列

基於JDK的實現方法,將未支付的訂單放到一個有序的隊列中,程序會自動依次取出過時的訂單。
若是當前沒有過時的訂單,就會阻塞,直至有過時的訂單。因爲每次只處理過時的訂單,而且處理的時間也很精準,不存在定時調度方案的那兩個弊端。
實現:
1.首先建立一個訂單類OrderDelayDto須要實現Delayed接口。而後重寫getDelay()方法和compareTo()方法,只加了訂單編號和過時時間兩個屬性。
這兩個方法很重要,
getDelay()方法實現過時的策略,好比,訂單的過時時間等於當前時間就是過時,返回負數就表明須要處理。不然不處理。
compareTo()方法實現訂單在隊列中的排序規則,這樣即便後面加入的訂單,也能加入到排序中,我這裏寫的規則是按照過時時間排序,最早過時的排到最前面,這一點很重要,由於排在最前面的若是沒有被處理,就會進入阻塞狀態,後面的不會被處理。springboot

import lombok.Data;
import java.util.Date;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
 * @author mashu
 * Date 2020/5/17 16:25
 */
@Data
public class OrderDelayDto implements Delayed {
    /**
     * 訂單編號
     */
    private String orderCode;
    /**
     * 過時時間
     */
    private Date expirationTime;

    /**
     * 判斷過時的策略:過時時間大於等於當前時間就算過時
     *
     * @param unit
     * @return
     */
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(this.expirationTime.getTime() - System.currentTimeMillis(), TimeUnit.NANOSECONDS);
    }

    /**
     * 訂單加入隊列的排序規則
     *
     * @param o
     * @return
     */
    @Override
    public int compareTo(Delayed o) {
        OrderDelayDto orderDelayDto = (OrderDelayDto) o;
        long time = orderDelayDto.getExpirationTime().getTime();
        long time1 = this.getExpirationTime().getTime();
        return time == time1 ? 0 : time < time1 ? 1 : -1;
    }
}

其實這樣已經算是寫好了。我沒有耍你。
寫個main 方法測試一下,建立兩個訂單o1和o2,放入到延時隊列中,而後while()方法不斷的去取。
在此方法內經過隊列的take()方法得到已過時的訂單,而後作出相應的處理。多線程

public static void main(String[] args) {
        DelayQueue<OrderDelayDto> queue = new DelayQueue<>();
        OrderDelayDto o1 = new OrderDelayDto();
        //第一個訂單,過時時間設置爲一分鐘後
        o1.setOrderCode("1001");
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MINUTE, 1);
        o1.setExpirationTime(calendar.getTime());
        OrderDelayDto o2 = new OrderDelayDto();
        //第二個訂單,過時時間設置爲如今
        o2.setOrderCode("1002");
        o2.setExpirationTime(new Date());
        //往隊列中放入數據
        queue.offer(o1);
        queue.offer(o2);
        // 延時隊列
        while (true) {
            try {
                OrderDelayDto take = queue.take();
                System.out.println("訂單編號:" + take.getOrderCode() + " 過時時間:" + take.getExpirationTime());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

運行結果:
image.pngide

我故意把第二個訂單的過時時間設置爲第一個訂單以前,從結果能夠看出,他們已經自動排序把最早過時的排到了最前面。
第一個訂單的失效時間是當前時間的後一分鐘,結果也顯示一分鐘後處理了第一條訂單。測試

2.然而一般狀況下,咱們會使用多線程去取延時隊列中的數據,這樣即便線程啓動以後也能動態的向隊列中添加訂單。
建立一個線程類OrderCheckScheduler實現Runnable接口,
添加一個延時隊列屬性,重寫run()方法,在此方法內經過隊列的take()方法得到已過時的訂單,而後作出相應的處理。優化

import java.util.concurrent.DelayQueue;
/**
 * @author mashu
 * Date 2020/5/17 14:27
 */
public class OrderCheckScheduler implements Runnable {

  // 延時隊列
  private DelayQueue<OrderDelayDto> queue;

  public OrderCheckScheduler(DelayQueue<OrderDelayDto> queue) {
      this.queue = queue;
  }

  @Override
  public void run() {
      while (true) {
          try {
              OrderDelayDto take = queue.take();
              System.out.println("訂單編號:" + take.getOrderCode() + " 過時時間:" + take.getExpirationTime());
          } catch (InterruptedException e) {
              e.printStackTrace();
          }
      }
  }
}

好了,寫個方法測試一下:this

public static void main(String[] args) {
        // 建立延時隊列
        DelayQueue<OrderDelayDto> queue = new DelayQueue<>();
        OrderDelayDto o1 = new OrderDelayDto();
        //第一個訂單,過時時間設置爲一分鐘後
        o1.setOrderCode("1001");
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.MINUTE, 1);
        o1.setExpirationTime(calendar.getTime());
        OrderDelayDto o2 = new OrderDelayDto();
        //第二個訂單,過時時間設置爲如今
        o2.setOrderCode("1002");
        o2.setExpirationTime(new Date());
        //運行線程
        ExecutorService exec = Executors.newFixedThreadPool(1);
        exec.execute(new OrderCheckScheduler(queue));
        //往隊列中放入數據
        queue.offer(o1);
        queue.offer(o2);
        exec.shutdown();
    }

結果和上面的同樣,圖就不截了,相信我。spa

過時提醒

基於redis的過時提醒功能,聽名字就知道這個方案最是純真、最直接的,就是單純處理過時的訂單。
修改個redis的配置吧先,由於redis默認不開啓過時提醒。
notify-keyspace-events改成notify-keyspace-events "Ex"
寫一個類用來接收來自redis的暖心提醒OrderExpirationListener,繼承一下KeyExpirationEventMessageListener抽象類。重寫onMessage()方法,在此方法中處理接收到的過時key.

import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;
import java.util.Date;

/**
 * @author mashu
 * Date 2020/5/17 23:01
 */
@Component
public class OrderExpirationListener extends KeyExpirationEventMessageListener {

    public OrderExpirationListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        final String expiredKey = message.toString();
        System.out.println("我過時了" + expiredKey+"當前時間:"+new Date());
    }
}

ok,向redis中存入一個訂單,過時時間爲1分鐘。

redis.set("orderCode/10010", "1", 1L, TimeUnit.MINUTES);
System.out.println("redis存入訂單號 key: orderCode/10010,value:1,過時時間一分鐘,當前時間"+new Date());

運行結果:
image.png

除此以外還有用到消息隊列的。夜深了,我得玩會遊戲了。

沒有絕對的好方案,只有在不一樣場景下的更合適的方案。隨着需求的變化,技術的革新,方案也會不斷的被優化和迭代,惟一不變的是工資。

相關文章
相關標籤/搜索