【原创】我还是很建议你用DelayQueue搞定超时订单的(1)

北城以北 提交于 2020-11-22 13:22:36

【原创】我还是很建议你用DelayQueue搞定超时订单的(1)

我就是那个人见人爱的 锦成同学,我是java进阶架构师社区的特邀作者,
今天为大家带来新的一篇小知识,祝各位宝宝能学到新知识...更上一层楼.....

【原创】我还是很建议你用DelayQueue搞定超时订单的(1)

一、用三根鸡毛做引言

  • 真的! 不骗你们的喔~ 相信大家都遇到类似于:订单30min后未支付自动取消的开发任务
  • 那么今日份就来了解一下怎么用延时队列 DelayQueue搞定单机版的超时订单

【原创】我还是很建议你用DelayQueue搞定超时订单的(1)

二、延时队列使用场景

那么什么时候需要用延时队列呢?常见的延时任务场景 举栗子:
订单在30分钟之内未支付则自动取消。
重试机制实现,把调用失败的接口放入一个固定延时的队列,到期后再重试。
新创建的店铺,如果在十天内都没有上传过商品,则自动发送消息提醒。
用户发起退款,如果三天内没有得到处理则通知相关运营人员。
预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议。
关闭空闲连接,服务器中,有很多客户端的连接,空闲一段时间之后需要关闭之。
清理过期数据业务。比如缓存中的对象,超过了空闲时间,需要从缓存中移出。
多考生考试,到期全部考生必须交卷,要求时间非常准确的场景。







三、解决办法多如鸡毛

定期轮询(数据库等)
JDK DelayQueue
JDK Timer
ScheduledExecutorService 周期性线程池
时间轮(kafka)
时间轮(Netty的HashedWheelTimer)
Redis有序集合(zset)
zookeeper之curator
RabbitMQ
Quartz,xxljob等定时任务框架
Koala(考拉)
JCronTab(仿crontab的java调度器)
SchedulerX(阿里)
有赞延迟队列
.....(鸡毛)
解决问题方法真是不胜枚举,正所谓一呼百应,一千个读者眼里有一千个哈姆雷特














  • 那我们第一篇先来实战JDK的DelayQueue,万祖归宗,万法同源,学会了最基础的Queue,就不愁其他的了
  • 后续再写几篇使用Redis,Zk,MQ的一些机制,实战分布式情况下的使用

    四、先认亲

延时队列,首先,它是一种队列,队列意味着内部的元素是有序的,元素出队和入队是有方向性的,元素从一端进入,从另一端取出。
其次,延时队列,最重要的特性就体现在它的延时属性上,跟普通的队列不一样的是,普通队列中的元素总是等着希望被早点取出处理,而延时队列中的元素则是希望被在指定时间得到取出和处理,所以延时队列中的元素是都是带时间属性的,通常来说是需要被处理的消息或者任务。
一言以蔽之曰 : 延时队列就是用来存放需要在指定时间被处理的元素的队列。
1) DelayQueue 是谁,上族谱
【原创】我还是很建议你用DelayQueue搞定超时订单的(1)
看的出来到DelayQueue这一代已经第五代传人了,
要知道 DelayQueue自幼生在八戒家,长大就往外面拉,熊熊烈火它不怕,水是水来渣是渣。
不过它真的是文韬武略,有一把ReentrantLock就是它的九齿钉耙,抗的死死の捍卫着自己的PriorityQueue.
有典故曰:







public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
           implements BlockingQueue<E> {
// 用于控制并发的 可重入 全局 锁
private final transient ReentrantLock lock = new ReentrantLock();
// 根据Delay时间排序的 ***的 优先级队列
private final PriorityQueue<E> q = new PriorityQueue<E>();
// 用于优化阻塞通知的线程元素leader,标记当前是否有线程在排队(仅用于取元素时)
private Thread leader = null;
// 条件,用于阻塞和通知的Condition对象,表示现在是否有可取的元素
private final Condition available = lock.newCondition();

       /**
        * 省洛方法代码.....  你们懂我的省洛吗?
        */
  • 注释的已经很清楚他们的意思了,也具备了并发编程之艺术的 锁,队列,状态(条件)

  • 他的几个方法也是通过 锁-->维护队列-->出队,入队-->根据Condition进行条件的判断-->进行线程之间的通信和唤起
  • 以支持优先级***队列的PriorityQueue作为一个容器,容器里面的元素都应该实现Delayed接口,在每次往优先级队列中添加元素时以元素的过期时间作为排序条件,最先过期的元素放在优先级最高。
  • DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
    2) 优先级队列 PriorityQueue
    因为我们的DelayQueue里面维护了一个优先级的队列PriorityQueue简单的看下:

    
    //默认容量11
     private static final int DEFAULT_INITIAL_CAPACITY = 11;
    //存储元素的地方 数组
    transient Object[] queue; // non-private to simplify nested class access
    //元素个数
    private int size = 0;
    //比较器
    private final Comparator<? super E> comparator;

    默认容量是11;
    queue,元素存储在数组中,这跟我们之前说的堆一般使用数组来存储是一致的;
    comparator,比较器,在优先级队列中,也有两种方式比较元素,一种是元素的自然顺序,一种是通过比较器来比较;
    modCount,修改次数,有这个属性表示PriorityQueue也是fast-fail的;
    PriorityQueue不是有序的,只有堆顶存储着最小的元素;
    PriorityQueue 是非线程安全的;
    3) DelayQueue的方法简介





  • 入队方法 : 若添加的元素是队首(堆顶)元素,就把leader置为空,并唤醒等待在条件available上的线程;
    public boolean add(E e) { return offer(e);}
public void put(E e) {    offer(e);}
public boolean offer(E e, long timeout, TimeUnit unit) {    return offer(e);}
public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();   //加锁 因为优先队列线程不安全
    try {
        q.offer(e);  //判断优先级 进行入队
    if (q.peek() == e) {    //-----[1]
        //leader记录了被阻塞在等待队列头生效的线程 新增一个元素到队列头,
        //表示等待原来队列头生效的阻塞的线程已经失去了阻塞的意义
        //,此时需要获取新的队列头进行返回了
        leader = null;
        //获取队列头的线程被唤起,主要有两种场景:
        //1. 之前队列为空,导致被阻塞的线程
        //2. 之前队列非空,但是队列头没有生效(到期)导致被阻塞的线程
        available.signal();
    }
        return true; //因为是***队列 所以添加元素肯定成功  直到OOM
    } finally {
        lock.unlock();   //释放锁
    }
}

offer()方法,首先获取独占锁,然后添加元素到优先级队列,由于q是优先级队列,所以添加元素后,peek并不一定是当前添加的元素,如果[1]为true,说明当前元素e的优先级最小也就即将过期的,这时候激活avaliable变量条件队列里面的线程,通知他们队列里面有元素了。

  • 出队方法 take()
    请看我详细的注释,绝不是蜻蜓点水
    public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock; //获取锁
    lock.lockInterruptibly();   //可中断锁 可以自行了解一下 嘻嘻嘻嘻...
    try {
        for (;;) {//会写死循环的都是高手
            E first = q.peek();//get队头元素
            if (first == null)
                // 队列头为空,则阻塞,直到新增一个入队为止(1)
                available.await();
            else {
                long delay = first.getDelay(NANOSECONDS);//获取剩余时间
                if (delay <= 0)
                    // 若队列头元素已生效,则直接返回(2)
                    return q.poll();
                first = null; // don't retain ref while waiting 等待的时候不能引用,表示释放当前引用的(3)
                if (leader != null)
                    // leader 非空时,表示有其他的一个线程在出队阻塞中 (4.1)
                    // 此时挂住当前线程,等待另一个线程出队完成
                    available.await();
                else {
                    //标识当前线程处于等待队列头生效的阻塞中 (4.2.1)
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        // 等待队列头元素生效(4.2.2)
                        available.awaitNanos(delay);
                    } finally {
                        //最终释放当前的线程 设置leader为null (4.2.3)
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }     //(5)
    } finally {
        if (leader == null && q.peek() != null)
            // 当前线程出队完成,通知其他出队阻塞的线程继续执行(6)
            available.signal();
            lock.unlock();//解锁结束
    }
    }

    那么,下面的结论肉眼可见:
    如果队列为空,则阻塞,直到有个线程(生产者投递数据)完成入队操作
    获取队列头,若队列头已生效,则直接返回
    未生效则释放当前引用
    当队列头部没有生效时候:




  • 标识当前线程处于等待队列头生效的阻塞中(leader = thisThread)
  • 阻塞当前线程,等待队列头生效
  • 队列头生效之后,清空标识(leader=null)
  • 若有另一个线程已经处于等待队列头生效的阻塞过程中,则阻塞当前线程,直到另一个线程完成出队操作
  • 若没有其他线程阻塞在出队过程中,即当前线程为第一个获取队列头的线程
    再次进入循环,获取队列头并返回
    最后,当前线程出队完成,通知其他出队阻塞的线程继续执行

4)Delayed
public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);
}

据情报显示:Delayed是一个继承自Delayed的接口,并且定义了一个Delayed方法,用于表示还有多少时间到期,到期了应返回小于等于0的数值。
很简答就是定义了一个,一个哈,一个表延迟的接口,就是个规范接口,目的就是骗我们去实现它的方法.哼~

五、再实战


说了那么多废话,让我想起了那句名言:一切没有代码实操的讲解都是耍流氓 至今深深的烙在我心中,所以我一定要实战给你们看,显得我不是流氓...

  • 实战以 订单下单后三十分钟内未支付则自动取消 为业务场景
  • 该场景的代码逻辑分析如下:
  • 下单后将订单直接放入未支付的延时队列中
  • 如果超时未支付,则从队列中取出,进行修改为取消状态的订单
  • 如果支付了,则不去进行取消,或者取消的时候做个状态筛选,即可避免更新
  • 或者支付完成后,做个主动出队
  • 还有就是用户主动取消订单,也做个主动出队
  • 那么我们写代码一定要通用,先来写个通用的Delayed 通用...嗯! 泛型的
import lombok.Getter;
import lombok.Setter;

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

/**
 * @author LiJing
 * @ClassName: ItemDelayed
 * @Description: 数据延迟实现实例 用以包装具体的实例转型
 * @date 2019/9/16 15:53
 */

@Setter
@Getter
public class ItemDelayed<T> implements Delayed {

    /**默认延迟30分钟*/
    private final static long DELAY = 30 * 60 * 1000L;
    /**数据id*/
    private Long dataId;
    /**开始时间*/
    private long startTime;
    /**到期时间*/
    private long expire;
    /**创建时间*/
    private Date now;
    /**泛型data*/
    private T data;

    public ItemDelayed(Long dataId, long startTime, long secondsDelay) {
        super();
        this.dataId = dataId;
        this.startTime = startTime;
        this.expire = startTime + (secondsDelay * 1000);
        this.now = new Date();
    }

    public ItemDelayed(Long dataId, long startTime) {
        super();
        this.dataId = dataId;
        this.startTime = startTime;
        this.expire = startTime + DELAY;
        this.now = new Date();
    }

    @Override
    public int compareTo(Delayed o) {
        return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }
}
  • 再写个通用的接口,用于规范和方便统一实现 这样任何类型的订单都可以实现这个接口 进行延时任务的处理
public interface DelayOrder<T> {

    /**
     * 添加延迟对象到延时队列
     *
     * @param itemDelayed 延迟对象
     * @return boolean
     */
    boolean addToOrderDelayQueue(ItemDelayed<T> itemDelayed);

    /**
     * 根据对象添加到指定延时队列
     *
     * @param data 数据对象
     * @return boolean
     */
    boolean addToDelayQueue(T data);

    /**
     * 移除指定的延迟对象从延时队列中
     *
     * @param data
     */
    void removeToOrderDelayQueue(T data);
}

六、后总结


  • 这就是单机的不好处,也是一个痛点,所以肯定是不太适合订单量特别大的场景 大家也要酌情考虑和运用
  • 相对于同等量级的数据库轮询操作来说,真是节省了不少数据库的压力和连接,还是值得一用的,我们可以只保存订单的id到延时实例中,这样缩减队列单个实例内存存储
  • 那还有技巧就是更新的时候注意控制好幂等性,控制好幂等性,会让你轻松很多,顺畅很多,但是数据量大了,要蛀牙的哦

那今日份的讲解就到此结束,具体的代码请移步我的gitHub的mybot项目Master分支[1]查阅,fork体验一把,或者评论区留言探讨,写的不好,请多多指教
原文地址:实战|我还是很建议你用DelayQueue搞定超时订单的[2]
参考资料

[1]
gitHub的mybot项目Master分支:
https://github.com/leaJone/mybot
[2]
掘金地址: https://juejin.im/post/5d822b7a6fb9a06b3260a9e6"
———— e n d ————
微服务、高并发、JVM调优、面试专栏等20大进阶架构师专题请关注公众号【Java进阶架构师】后在菜单栏查看。
【原创】我还是很建议你用DelayQueue搞定超时订单的(1)
看到这里,说明你喜欢本文
你的转发,是对我最大的鼓励!在看亦是支持↓






易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!