业务专题-延时任务

延时任务

延时任务的场景:以订单支付为例,下单后订单更新为待支付状态;进入支付环节(一般会设置支付超时时间,下单后商品库存会锁定,长时间不支付需要取消订单)后,点击支付并完成则订单更新为支付完成状态,如果超时未支付则更新订单为取消状态。

其中订单超时后的状态修改为延时任务:即订单提交后一方面修改订单为待支付,一方面设置延时任务。当订单支付超时修改为取消;如果在取消前完成支付则需要取消定时任务,或者更新订单为取消状态时需要判断当前订单是否为待支付状态(已支付则不修改为取消)。

解决方案

状态映射

通过订单当前状态、当前时间-订单提交时间(判断是否超时),对待支付的订单状态进行判断,根据前述判断将订单状态由待支付,映射为支付完成或者取消。

此方式的优点是状态改变无延迟;缺点是所有相关地方都需要通过此种方式转换状态,代码量大,并且对于特定情况下订单列表查询是会变的特别复杂。所以一般不建议使用,除非是特别简单的业务。

定时任务

即通过时间间隔短的定时任务扫描所有待支付状态的订单,一旦扫描到支付超时的订单,即修改订单状态为取消。

优点是代码简单;缺点是定时任务间隔大则延时任务不准确,定时任务间隔小则影响服务器性能。

延时队列

订单提交后,提交任务到延时队列中,超时后则更新订单状态。

缺点是需要考虑多种特定情况:例如服务器宕机后缓存中的延时队列如何重启,延时队列任务是否会丢失等;优点是延时任务精确、相比较定时任务对服务器性能影响小。

如下通过延时队列解决定时超时支付的问题,并对可能出现的问题进行讨论:

  1. 核心步骤是将订单包装为延时任务,放入延时队列中;然后在异步线程中处理延时队列中获取的延时任务,获取到延时任务后更新定位为支付超时。

  2. 延时任务的包装,关键是实现延时时间计算的方法,此方法需要和当前时间计算后返回一个动态的延时剩余时间。

  3. 重启服务导致延时任务丢失的问题,这种情况下需要在生成延时任务的同时将延时任务持久化到数据库中,延时任务执行完毕后从数据库删除延时任务;如果服务重启,则重启后先从数据库读取延时任务并进行处理。

  4. 如果订单支付完成,是否需要从延时队列删除任务?理论上不需要删除延时任务,则处理延时任务的是否可以增加状态判断,进未支付的订单进行状态更新;否则需要封装一个方法从延时队列中删除任务。

  5. 如果存在一个订单同时被更新为支付完成和取消支付,则此时需要在更新订单时判断订单状态确保不会因为并发导致订单状态更新被覆盖的问题

package com.huobi.hl;

import lombok.*;
import org.jetbrains.annotations.NotNull;

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

/**
 * hlTodo
 *
 * @author hulei
 * @date 2021/4/8
 */
public class DelayOrderTest {

    public static DelayQueue<DelayTask> delayQueue = new DelayQueue<>();

    public static final int delaySeconds = 10;

    @SneakyThrows
    public static void main(String[] args) {
        // 订单1修改为已提交,支付超时时间10s
        delayQueue.offer(new DelayTask(Order.builder().orderId("1").submitTime(new Date()).build(), delaySeconds));
        Thread.sleep(2000);
        // 2s后订单2修改为已提交,支付超时时间10s
        delayQueue.offer(new DelayTask(Order.builder().orderId("2").submitTime(new Date()).build(), delaySeconds));

        // 异步线程中处理延时队列
        Thread thread = new Thread(() -> {
            while (true) {
                DelayTask task = null;
                try {
                    // 阻塞等待
                    task = delayQueue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                // 超时后获取到延时任务,更新订单状态
                System.out.println(task);
            }
        });
        thread.start();
    }

    /**
     * 延时任务
     */
    public static class DelayTask implements Delayed {
        /**
         * 订单
         */
        private final Order order;

        /**
         * 订单超时时间(单位:s)
         */
        private final Integer delaySeconds;

        public DelayTask(Order order, int delaySeconds) {
            this.order = order;
            this.delaySeconds = delaySeconds;
        }

        /**
         * 初始化当前延时任务延时时间
         * @param unit 时间工具, 将当前任务的延时时间修改为毫秒值
         * @return 延时时间毫秒值
         */
        @Override
        public long getDelay(@NotNull TimeUnit unit) {
            // 用延时任务截止时间点 - 当前时间,此函数返回值必须和当前时间相关,不能为固定值
            long dynamic = (order.getSubmitTime().getTime() + delaySeconds * 1000) - System.currentTimeMillis();
            return unit.convert(dynamic, TimeUnit.MILLISECONDS);
        }

        @Override
        public int compareTo(@NotNull Delayed o) {
            // 通过getDelay方法计算延时任务出队时间
            DelayTask o1 = (DelayTask) o;
            // 时间差转换位毫秒差值
            long l = this.getDelay(TimeUnit.MILLISECONDS) - o1.getDelay(TimeUnit.MILLISECONDS);
            return l == 0 ? 0 : (l > 0 ? 1 : -1);
        }

        @Override
        public String toString() {
            return System.currentTimeMillis() + " >>> orderId >>> " + order.getOrderId();
        }
    }

    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Order {
        /**
         * 订单id
         */
        private String orderId;
        /**
         * 订单状态, 0待支付,-1取消,1支付成功
         */
        private String status;
        /**
         * 订单提交时间
         */
        private Date submitTime;
    }
}
posted @ 2021-04-08 14:46  规划中~~~  阅读(157)  评论(0)    收藏  举报