电商系统中,有这样的需求,用户下单三小时未支付就自动取消,具体如何实现的呢?
一、实现方案
通常实现方案有以下方式:
-
方式一
使用定时任务不断轮询取消,此种方式实现简单,但是存在一个问题,定时任务设置时间较短时,耗费资源,设置时间过长,则会导致有一些订单超过三小时很久才能取消,用户体验不好
-
方式二
在拉取我的订单时,进行判断然后做取消操作,此种方法,用户体验较好,但是在拉取订单列表的时候耦合了取消订单的操作,从系统的设计角度考虑不是很好。
-
方式三
使用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是存在本地缓存中的,本地缓存存取数量有限,过多的待取消订单,也许可能将订单服务内存空间占用完,如此,会影响到服务的使用。