1. 程式人生 > >三小時未付款自動取消訂單實現

三小時未付款自動取消訂單實現

電商系統中,有這樣的需求,使用者下單三小時未支付就自動取消,具體如何實現的呢?

一、實現方案

通常實現方案有以下方式:

  • 方式一

使用定時任務不斷輪詢取消,此種方式實現簡單,但是存在一個問題,定時任務設定時間較短時,耗費資源,設定時間過長,則會導致有一些訂單超過三小時很久才能取消,使用者體驗不好

  • 方式二

在拉取我的訂單時,進行判斷然後做取消操作,此種方法,使用者體驗較好,但是在拉取訂單列表的時候耦合了取消訂單的操作,從系統的設計角度考慮不是很好。

  • 方式三

使用DelayQueue佇列和redis以及監聽器設計,此種方式使用者體驗好,與其他功能耦合性低,但是使用者量有所限制

二、具體實現

方式一、方式二都比較容易實現,這裡不再講述,本文講述一下方式三的實現。

1、DelayQueue 延時佇列,此佇列放入的資料需要實現java.util.concurrent.Delayed介面,使用者存放待取消訂單

2、redis 分散式快取,用於存放待取消訂單,資料可以長久存放,不受服務啟停影響

3、監聽器,監聽某一事件,執行一定的邏輯

程式碼實現如下:

  • 取消訂單service 類
package com.bsqs.shop.order.entity.vo;

import com.alibaba.fastjson.JSONObject;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.commons.lang.math.NumberUtils;

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
 * @program: i5-project
 * @description:
 * @author: congming wang
 * @create: 2018-09-05 15:52
 **/
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class OrderAutoEntity implements Delayed{

    private String orderId;
    private long startTime;

    @Override
    public int compareTo(Delayed other) {
        if (other == this){
            return 0;
        }
        if(other instanceof OrderAutoEntity){
            OrderAutoEntity otherRequest = (OrderAutoEntity)other;
            long otherStartTime = otherRequest.getStartTime();
            return (int)(this.startTime - otherStartTime);
        }
        return 0;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(startTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;

        result = prime * result + (int) (NumberUtils.createInteger(orderId) ^ (NumberUtils.createInteger(orderId) >>> 32));
        result = prime * result + (int) (startTime ^ (startTime >>> 32));
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        OrderAutoEntity other = (OrderAutoEntity) obj;
        if (orderId != other.orderId)
            return false;
        if (startTime != other.startTime)
            return false;
        return true;
    }

    @Override
    public String toString() {
        return JSONObject.toJSONString(this);
    }
}
package com.bsqs.shop.order.service.impl;

import com.bsqs.shop.order.dao.BsqsOrderDao;
import com.bsqs.shop.order.dao.BsqsOrderRecordDao;
import com.bsqs.shop.order.entity.BsqsOrder;
import com.bsqs.shop.order.entity.BsqsOrderRecord;
import com.bsqs.shop.order.entity.vo.OrderAutoEntity;
import com.bsqs.shop.order.rao.RedisRao;
import com.bsqs.shop.order.service.OrderAutoCancelService;
import com.bsqs.shop.order.util.Constants;
import com.bsqs.shop.order.util.OrderStatus;
import com.bsqs.shop.order.util.lock.RedisLockManager;
import com.wangcongming.util.CollectionUtils;
import com.wangcongming.util.IDUtil;
import com.wangcongming.util.ThreadPoolExecutorUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.ExecutorService;

/**
 * @program: i5-project
 * @description: 3小時未支付自動取消
 * @author: congming wang
 * @create: 2018-09-05 15:48
 **/
@Service
@Slf4j
public class OrderAutoCancelServiceImpl implements OrderAutoCancelService {

    @Autowired
    private BsqsOrderDao orderDao;
    @Autowired
    private BsqsOrderRecordDao orderRecordDao;
    @Autowired
    private RedisLockManager redisLockManager;
    @Autowired
    private RedisRao redisRao;

    //用於放入需要自動取消的訂單
    private final static DelayQueue<OrderAutoEntity> delayQueue = new DelayQueue<OrderAutoEntity>();

    private boolean start;

    /**
     * 取消訂單
     */
    @Override
    public void start(){
        if(start){
            log.debug("OrderAutoCancelServiceImpl 已啟動");
            return;
        }
        start = true;
        log.debug("OrderAutoCancelServiceImpl 啟動成功");
        new Thread(()->{
            try {
                while(true){
                    OrderAutoEntity order = delayQueue.take();
                    ExecutorService threadPool = ThreadPoolExecutorUtil.getThreadPool(null, 100);
                    threadPool.execute(()->{cancelOrder(order);});
                }
            } catch (InterruptedException e) {
                log.error("InterruptedException error:",e);
            }
        }).start();
    }

    private void cancelOrder(OrderAutoEntity order){
        String orderId = order.getOrderId();
        String lockKey = String.format("%s%s",Constants.RedisKey.ORDER_AUTO_CANCEL_UPDATE,orderId);
        try {
            if(redisLockManager.tryLock(lockKey)){
                Object value = redisRao.getHashKeyObjValue(Constants.RedisKey.ORDER_AUTO_CANCEL_HASH, orderId);
                if(value == null){
                    log.info(">>>>>>>>>>>>redis中不存在該訂單,訂單:{}已經被取消,不處理<<<<<<<<<<<<",orderId);
                    return;
                }
                updateOrder(orderId);
                //刪除redis資料
                log.info("取消訂單。。。。開始刪除redis資料 order:{}",orderId);
                redisRao.removeHashValue(Constants.RedisKey.ORDER_AUTO_CANCEL_HASH,orderId);
                log.info("取消訂單。。。。刪除redis資料成功 order:{}",orderId);
            }
        } catch (Exception e) {
            log.error(">>>>>>>>>訂單:{}取消發生異常,",e);
        } finally {
            redisLockManager.unlock(lockKey);
        }
    }

    @Transactional(rollbackFor = Exception.class)
    public void updateOrder(String orderId){
        BsqsOrder order = new BsqsOrder();
        order.setOrderId(orderId);

        List<BsqsOrder> orders = this.orderDao.findByEntity(order);
        if(CollectionUtils.isEmpty(orders)){
            log.info(">>>>>>>>>>>>訂單:{}不存在<<<<<<<<<<<<",orderId);
            return;
        }

        BsqsOrder entity = orders.get(0);
        if(entity.getOrderStatus().equals(OrderStatus.CANCEL_ORDER.getCode())){
            log.info(">>>>>>>>>>>>訂單:{}已經被取消,不處理<<<<<<<<<<<<",orderId);
            return;
        }

        log.info("自動取消訂單 ------ 開始取消:order={}",orderId);
        //根據orderId查詢訂單
        BsqsOrder update = new BsqsOrder();
        update.update("system_cancel");
        update.setStatus(OrderStatus.CANCEL_ORDER);
        this.orderDao.updateEntityById(entity);
        BsqsOrderRecord record = new BsqsOrderRecord();
        record.setRecordId(IDUtil.genCode("OR"));
        record.setUserId("system_cancel");
        record.setOrderId(orderId);
        record.setOrderStatus(OrderStatus.CANCEL_ORDER);
        record.setDelFlag("0");
        record.pre("system_cancel");
        this.orderRecordDao.saveEntity(record);
        log.info("自動取消訂單 ------ 訂單取消成功:order={}",orderId);
    }

    /**
     * 新增待取消訂單
     * @param entity
     */
    @Override
    public void add(OrderAutoEntity entity){
        delayQueue.put(entity);
        redisRao.setHashValue(Constants.RedisKey.ORDER_AUTO_CANCEL_HASH, entity.getOrderId(),entity);
    }

    /**
     * 新增待取消訂單
     * @param orderId 訂單號
     * @param timeout 過期時間
     */
    @Override
    public void add(String orderId,long timeout){
        OrderAutoEntity entity = new OrderAutoEntity(orderId, timeout);
        delayQueue.put(entity);
        redisRao.setHashValue(Constants.RedisKey.ORDER_AUTO_CANCEL_HASH, orderId,entity);
    }

    /**
     * 移除
     * @param entity
     */
    @Override
    public void remove(OrderAutoEntity entity){
        delayQueue.remove(entity);
        redisRao.removeHashValue(Constants.RedisKey.ORDER_AUTO_CANCEL_HASH, entity.getOrderId());
    }

    /**
     * 移除
     * @param orderId 訂單號
     * @param timeout
     */
    @Override
    public void remove(String orderId,long timeout){
        delayQueue.remove(new OrderAutoEntity(orderId,timeout));
        redisRao.removeHashValue(Constants.RedisKey.ORDER_AUTO_CANCEL_HASH, orderId);
    }

    /**
     * 移除
     * @param orderId 訂單號
     */
    @Override
    public void remove(String orderId){
        OrderAutoEntity entity = (OrderAutoEntity)redisRao.getHashKeyObjValue(Constants.RedisKey.ORDER_AUTO_CANCEL_HASH, orderId);
        delayQueue.remove(entity);
        redisRao.removeHashValue(Constants.RedisKey.ORDER_AUTO_CANCEL_HASH, orderId);
    }
}

程式碼中 start()方法用於取消訂單功能的真正實現,只需要在系統啟動的時候啟動才方法,則會自動啟動一個執行緒,監聽著佇列DelayQueue,一旦有資料到期,自動吐出資料,然後執行取消操作,取消訂單之後將資料從redis中移除

add()方法,用於在使用者下完訂單之後,將訂單加入到待取消訂單列表,redis 和佇列同時加入

remove()方法,用於在使用者完成支付之後,將訂單從待取消列表中移除

  • 監聽器實現

以上已經實現訂單取消,那麼如何將start()方法在服務啟動成功之後啟動呢?

這就需要使用到監聽器了,spring的監聽器會在容器啟動成功之後執行,所以只需要實現一個監聽器即可,具體實現如下:

package com.bsqs.shop.order.listener;

import com.bsqs.shop.order.entity.vo.OrderAutoEntity;
import com.bsqs.shop.order.rao.RedisRao;
import com.bsqs.shop.order.service.OrderAutoCancelService;
import com.bsqs.shop.order.util.Constants;
import com.wangcongming.util.CollectionUtils;
import com.wangcongming.util.ThreadPoolExecutorUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
@Slf4j
public class OrderAutoCancelListener implements ApplicationListener<ContextRefreshedEvent> {

	@Autowired
	private OrderAutoCancelService orderAutoCancelService;
    @Autowired
    private RedisRao redisRao;
	
    @Override
    public void onApplicationEvent(ContextRefreshedEvent evt) {
    	log.info(">>>>>>>>>>>>系統啟動完成,開始載入訂單自動取消功能onApplicationEvent()<<<<<<<<<<<<<<<");
        if (evt.getApplicationContext().getParent() == null) {
            return;
        }
        //自動取消
		orderAutoCancelService.start();
        //查詢需要入隊的訂單
        ThreadPoolExecutorUtil.getThreadPool(null, 100).execute(()->{
            log.error(">>>>>>>>>>>>查詢需要入隊的訂單<<<<<<<<<<<<<<<<<<<<");
            Map<String, Object> entities = redisRao.getHashValue(Constants.RedisKey.ORDER_AUTO_CANCEL_HASH);
            if(CollectionUtils.isEmpty(entities)){
                log.info(">>>>>>>>>>沒有查詢到待取消訂單<<<<<<<<<<<<<<");
                return;
            }
            entities.keySet().stream().forEach(item -> {
                OrderAutoEntity order = (OrderAutoEntity)entities.get(item);
                if(order == null){
                    return;
                }
                orderAutoCancelService.add(order);
            });
            log.info(">>>>>>>>>>待取消訂單入隊完成<<<<<<<<<<<<<<<<<<<<<<");
        });
    }
}

程式碼中可以看到,首先是啟動了OrderAutoCancelServiceImpl 中的start()方法,然後將redis中的資料回寫入DelayQueue佇列中,以便取消。

三、總結

為什麼說方式三不適合大使用者量,就是因為DelayQueue是存在本地快取中的,本地快取存取數量有限,過多的待取消訂單,也許可能將訂單服務記憶體空間佔用完,如此,會影響到服務的使用。