三小时未付款自动取消订单实现

电商系统中,有这样的需求,用户下单三小时未支付就自动取消,具体如何实现的呢?

一、实现方案

通常实现方案有以下方式:

  • 方式一

使用定时任务不断轮询取消,此种方式实现简单,但是存在一个问题,定时任务设置时间较短时,耗费资源,设置时间过长,则会导致有一些订单超过三小时很久才能取消,用户体验不好

  • 方式二

在拉取我的订单时,进行判断然后做取消操作,此种方法,用户体验较好,但是在拉取订单列表的时候耦合了取消订单的操作,从系统的设计角度考虑不是很好。

  • 方式三

使用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是存在本地缓存中的,本地缓存存取数量有限,过多的待取消订单,也许可能将订单服务内存空间占用完,如此,会影响到服务的使用。

猜你喜欢

转载自blog.csdn.net/linhui258/article/details/82668671
今日推荐