使用JAVA DelayQueue做延时队列

737 阅读3分钟

我正在参加「掘金·启航计划」

简介:

  DelayQueue是BlockingQueue的一种,所以它是线程安全的,DelayQueue的特点就是插入Queue中的数据可以按照自定义的delay时间进行排序。只有delay时间小于0的元素才能够被取出。

先看一下DelayQueue的定义:

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E>

从定义可以看到,DelayQueue中存入的对象都必须是Delayed的子类。

Delayed继承自Comparable,并且需要实现一个getDelay的方法。

DelayQueue的应用: 

  DelayQueue一般用于生产者消费者模式,我们下面举一个取消订单的具体的例子。

  首先要使用DelayQueue,必须自定义一个Delayed对象:

@Data
@TableName(value = "tb_order")
public class Order extends Model<Order> implements Delayed{

    /**
     * 订单号
     */
    @TableId(value = "order_no")
    private String orderNo;

    /**
     * 用户id
     */
    @TableField(value = "user_id")
    private String userId;

    /**
     * 订单状态(0待支付,1已支付,2已取消)
     */
    @TableField(value = "status")
    private Integer status;

    /**
     * 订单创建时间
     */
    @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(value = "create_time")
    private Date createTime;

    /**
     * 订单取消时间
     */
    @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(value = "cancel_time")
    private Date cancelTime;

    public Order() {
    }

    public Order(String orderNo, String userId, Integer status, Date createTime, Date cancelTime) {
        this.orderNo = orderNo;
        this.userId = userId;
        this.status = status;
        this.createTime = createTime;
        this.cancelTime = cancelTime;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        //下面用到unit.convert()方法,其实在这个小场景不需要用到,只是学习一下如何使用罢了
        return unit.convert(cancelTime.getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
        //这里根据取消时间来比较,如果取消时间小的,就会优先被队列提取出来
        return this.getCancelTime().compareTo(((Order) o).getCancelTime());
    }
}

 

因为DelayQueue的数据是保存在内存中的可能会丢失,这里创建了一个TrOrder的对象用了持久化队列的数据到数据库

@Data
@TableName(value = "tr_order")
public class TrOrder extends Model<TrOrder> {
    /**
     * 订单号
     */
    @TableId(value = "order_no")
    private String orderNo;

    /**
     * 用户id
     */
    @TableField(value = "user_id")
    private String userId;

    /**
     * 订单状态(0待支付,1已支付,2已取消)
     */
    @TableField(value = "status")
    private Integer status;

    /**
     * 订单创建时间
     */
    @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(value = "create_time")
    private Date createTime;

    /**
     * 订单取消时间
     */
    @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(value = "cancel_time")
    private Date cancelTime;

}

 

 

 

 创建一个取消订单的接口,编写具体实现:

public interface CancelOrderService {

    /**
     * 取消订单
     */
    void cancelOrder();

    /**
     * 获取队列
     * @return
     */
    DelayQueue<Order> getOrder();

    void updateQueue(String id);
}

实现类:

@Service
@Slf4j
public class CancelOrderServiceImpl implements CancelOrderService {

    /**
     * 是否开启自动取消功能
     */
    @Value("${order.isStarted}")
    private int isStarted;

    /**
     * 延迟队列,用来存放订单对象
     */
    DelayQueue<Order> queue = new DelayQueue<>();

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private static final String key = "delay:order";

    @Resource
    private ThreadPoolTaskExecutor executorService;

    @Override
    public void cancelOrder() {
        //新建一个线程,用来模拟定时取消订单job
        executorService.submit(()->{
            try {
                log.info("开启自动取消订单job,当前时间:" + DateUtil.date());
                List<Order> orderList = new ArrayList<>();
                while (true) {
                    String s = (String) redisTemplate.opsForList().rightPop(key);
                    Order order = JSON.parseObject(s, Order.class);
                    if (order != null) {
                        orderList.add(order);
                    } else {
                        break;
                    }
                }
                if (CollUtil.isNotEmpty(orderList)) {
                    merge(orderList);
                }
                while (isStarted == 1) {
                    try {
                        Order order = queue.take();
                        log.info("json:{}",JSON.toJSONString(order));
                        redisTemplate.opsForList().remove(key,0,JSON.toJSONString(order));
                        order.setStatus(2);
                        order.updateById();
                        log.info("订单:" + order.getOrderNo() + "付款超时,自动取消,当前时间:" + DateUtil.date());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }

    @Override
    public DelayQueue<Order> getOrder() {
        executorService.submit(()->{
            try {
                Date date = DateUtil.date();
                for (int i = 1; i < 6; i++) {
                    Order order = new Order("SO00"+i, "00"+i, 0,date, DateUtil.offset(date, DateField.MINUTE, 1+i));
                    order.insert();
                    Order order1 = order.selectById();
                    queue.add(order1);
                    // log.info("json:{}",JSON.toJSONString(order));
                    redisTemplate.opsForList().leftPush(key, JSON.toJSONString(order1));
                    //设置过期时间
                    redisTemplate.expire(key,48, TimeUnit.HOURS);
                    log.info("redis已保存");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
        return queue;

    }

    @Override
    public void updateQueue(String id) {
        Order order = new Order().selectOne(new QueryWrapper<Order>().eq("order_no", id));
        if (order != null) {
            // log.info("json:{}",JSON.toJSONString(order));
            redisTemplate.opsForList().remove(key,0,JSON.toJSONString(order));
            remove(id);
            order.setCancelTime(DateUtil.offset(DateUtil.date(), DateField.MINUTE, 1));
            order.updateById();
            Order order1 = order.selectById();
            boolean add = queue.add(order1);
            redisTemplate.opsForList().leftPush(key, JSON.toJSONString(order1));
            log.info(add+"");
        }
    }


    public void remove(String id){
        Order[] array = queue.toArray(new Order[]{});
        if(array == null || array.length <= 0){
            return;
        }
        Order target = null;
        for(Order order : array){
            if(order.getOrderNo().equals(id)){
                target = order;
                break;
            }
        }
        if(target != null){
            boolean remove = queue.remove(target);
            log.info(remove+"");
        }
    }

    public void merge(List<Order> list){
        log.info("进入redis合并队列");
        Order[] array = queue.toArray(new Order[]{});
        if(array == null || array.length <= 0){
            log.info("队列为空");
            for (Order order : list) {
                queue.add(order);
            }
        }
        List<String> ids = new ArrayList<>();
        Order target = null;
        for(Order order : array){
            if (!ids.contains(order.getOrderNo())) {
                ids.add(order.getOrderNo());
            }
        }
        if (CollUtil.isNotEmpty(ids)) {
            log.info("队列不为空");
            for (Order order : list) {
                if (!ids.contains(order.getOrderNo())) {
                    queue.add(order);
                }
            }
        }
    }

    public static void main(String[] args) {
        DateTime date = DateUtil.date();
        System.out.println(date);
        DateTime offset = DateUtil.offset(date, DateField.MINUTE, 1);
        System.out.println(offset);
    }
}

 

 

 这里编写取消订单的方法

@Override
public void cancelOrder() {
    //新建一个线程,用来模拟定时取消订单job
    executorService.submit(()->{
        try {
            log.info("开启自动取消订单job,当前时间:" + DateUtil.date());
            List<Order> orderList = new ArrayList<>();
            while (true) {
                String s = (String) redisTemplate.opsForList().rightPop(key);
                Order order = JSON.parseObject(s, Order.class);
                if (order != null) {
                    orderList.add(order);
                } else {
                    break;
                }
            }
            if (CollUtil.isNotEmpty(orderList)) {
                merge(orderList);
            }
            while (isStarted == 1) {
                try {
                    Order order = queue.take();
                    log.info("json:{}",JSON.toJSONString(order));
                    redisTemplate.opsForList().remove(key,0,JSON.toJSONString(order));
                    order.setStatus(2);
                    order.updateById();
                    log.info("订单:" + order.getOrderNo() + "付款超时,自动取消,当前时间:" + DateUtil.date());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    });
}

 

 

public void merge(List<Order> list){
    log.info("进入redis合并队列");
    Order[] array = queue.toArray(new Order[]{});
    if(array == null || array.length <= 0){
        log.info("队列为空");
        for (Order order : list) {
            queue.add(order);
        }
    }
    List<String> ids = new ArrayList<>();
    Order target = null;
    for(Order order : array){
        if (!ids.contains(order.getOrderNo())) {
            ids.add(order.getOrderNo());
        }
    }
    if (CollUtil.isNotEmpty(ids)) {
        log.info("队列不为空");
        for (Order order : list) {
            if (!ids.contains(order.getOrderNo())) {
                queue.add(order);
            }
        }
    }
}

 

 

 这里就是从项目启动后从数据库中读取有没有缓存的数据,有的话和队列合并,然后由队列取出数据修改订单状态,并且删除对应的缓存数据

 

这里创建一个模拟创建订单数据的方法


@Override
public DelayQueue<Order> getOrder() {
    executorService.submit(()->{
        try {
            Date date = DateUtil.date();
            for (int i = 1; i < 6; i++) {
                Order order = new Order("SO00"+i, "00"+i, 0,date, DateUtil.offset(date, DateField.MINUTE, 1+i));
                order.insert();
                Order order1 = order.selectById();
                queue.add(order1);
                // log.info("json:{}",JSON.toJSONString(order));
                redisTemplate.opsForList().leftPush(key, JSON.toJSONString(order1));
                //设置过期时间
                redisTemplate.expire(key,48, TimeUnit.HOURS);
                log.info("redis已保存");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    });
    return queue;

}

 

 

 然后要想让我们的任务在程序启动后执行需要任务实现CommandLineRunner接口 

@Component
@Order(-1)
public class StartComponent implements CommandLineRunner {

    @Autowired
    private CancelOrderService cancelOrderService;
    @Autowired
    private CancelOrderMysqlService cancelOrderMysqlService;

    @Override
    public void run(String... args) throws Exception {
        cancelOrderMysqlService.cancelOrder();
    }
}

 

然后启动添加5条数据看下执行效果:

 

 

 

 

 

 

 

 可以看到订单到取消时间后会自定执行取消订单的任务.