三小時未付款自動取消訂單實現
電商系統中,有這樣的需求,使用者下單三小時未支付就自動取消,具體如何實現的呢?
一、實現方案
通常實現方案有以下方式:
-
方式一
使用定時任務不斷輪詢取消,此種方式實現簡單,但是存在一個問題,定時任務設定時間較短時,耗費資源,設定時間過長,則會導致有一些訂單超過三小時很久才能取消,使用者體驗不好
-
方式二
在拉取我的訂單時,進行判斷然後做取消操作,此種方法,使用者體驗較好,但是在拉取訂單列表的時候耦合了取消訂單的操作,從系統的設計角度考慮不是很好。
-
方式三
使用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是存在本地快取中的,本地快取存取數量有限,過多的待取消訂單,也許可能將訂單服務記憶體空間佔用完,如此,會影響到服務的使用。