在这个电子商务流行年代,24小时随时随地网购已经成为我们习以为常的生活习惯。看到不错的商品,我们会立即下单,完全不受时间、空间的限制,剁手,然后在家坐等收快递,那种感觉。。。。

细心的你是否发现,我们好像很少去主动点击‘’确认收货”,毕竟拿到了货,我总要先体验几天,谁知道它会不会坏,但时间一拖就忘了操作确认收货。而担保交易,如果买家不确认收货,交易订单无法完结,那商家是收不到货款的。有什么解决办法?

上面示图是淘宝APP的订单详情页,左上方的自动确认时间起到了关键作用。也就是说,到了目标时间系统会自动触发代替买家执行确认收货。对上述的任务,我们给一个专业的名字,那就是延迟任务。那么这里你可能会问,这个延迟任务和定时任务的区别究竟在哪里呢?

1、定时任务有固定的触发时间(比如每天的凌晨2点执行),延迟任务的执行时间不固定,严格依赖于业务事件的触发时间(比如:自动确认收货是在卖家发货那个时刻往后延15天)

2、定时任务有执行周期,而延迟任务在某事件触发后一段时间内执行,一般是一次性的,没有执行周期

3、定时任务一般执行的是批处理操作多个任务,而延迟任务一般是单个任务

延迟任务的一些业务场景:

1、当你下了一笔订单后,一直没有付款,一般超过30分钟后,系统会自动关闭订单并退还库存

2、购买一件商品,如果你不喜欢会申请退款,当卖家超过3天未处理,系统会自动退款成功

3、生成订单60秒后,自动给用户发短信

延迟任务不仅仅适用于电商业务,对于预先设定目标执行时间,当时间到了需要自动触发执行的业务场景都可以参考该设计方案。下面我们具体讲一讲延迟任务常见的技术实现,后面工作中你可能会用的上。。。

一、JDK 延迟队列

通过JDK提供的DelayQueue 类来实现。DelayQueue 是一个无界阻塞队列,支持延时获取元素,队列中的元素必须实现 Delayed 接口,并重写 getDelay(TimeUnit) 和 compareTo(Delayed) 方法,代码示例如下:

public class DelayQueueTest {    public static void main(String[] args) {        DelayQueue<DelayTask> dq = new DelayQueue<DelayTask>();        //生产者生产一个2秒的延时任务        new Thread(new ProducerDelay(dq, 2000)).start();        //开启消费者轮询        new Thread(new ConsumerDelay(dq)).start();    }}class ProducerDelay implements Runnable {    DelayQueue<DelayTask> delayQueue;    int delaySecond;    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");    public ProducerDelay(DelayQueue<DelayTask> delayQueue, int delaySecond) {        this.delayQueue = delayQueue;        this.delaySecond = delaySecond;    }    @Override    public void run() {        for (int i = 1; i < 6; i++) {            delayQueue.add(new DelayTask(delaySecond, i + ""));            System.out.println(sdf.format(new Date()) + " Thread " + Thread.currentThread() + " 添加了一个延迟任务,id=" + i);            try {                Thread.sleep(500);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}class ConsumerDelay implements Runnable {    DelayQueue<DelayTask> delayQueue;    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");    public ConsumerDelay(DelayQueue<DelayTask> delayQueue) {        this.delayQueue = delayQueue;    }    @Override    public void run() {        while (true) {            DelayTask delayTask = null;            try {                delayTask = delayQueue.take();            } catch (Exception e) {                e.printStackTrace();            }            //如果Delay元素存在,则任务到达超时时间            if (delayTask != null) {                //处理任务                System.out.println(sdf.format(new Date()) + " Thread " + Thread.currentThread() + " 消费了一个延迟任务,id=" + delayTask.getId());            } else {                try {                    Thread.sleep(200);                } catch (InterruptedException e) {                }            }        }    }}@Data@AllArgsConstructorclass DelayTask implements Delayed {    String id;    // 延迟截止时间(单位:毫秒)    long delayTime = System.currentTimeMillis();    public DelayTask(long delayTime, String id) {        this.delayTime = (this.delayTime + delayTime);        this.id = id;    }    @Override    // 获取剩余时间    public long getDelay(TimeUnit unit) {        return unit.convert(delayTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);    }    @Override    // 队列里元素的排序依据    public int compareTo(Delayed o) {        if (this.getDelay(TimeUnit.MILLISECONDS) > o.getDelay(TimeUnit.MILLISECONDS)) {            return 1;        } else if (this.getDelay(TimeUnit.MILLISECONDS) < o.getDelay(TimeUnit.MILLISECONDS)) {            return -1;        } else {            return 0;        }    }@Override    public String toString() {        return DateFormat.getDateTimeInstance().format(new Date(delayTime));    }}

点评:

1、由于采用无界阻塞队列,占用本地内存,如果任务太多的话,很容易产生内存溢出(OOM)的风险;

2、另外该实现是单机版玩法,如果发生系统重启等情况会导致内存数据丢失,需要考虑将数据重新预热到缓存的操作,有额外实现成本

当然有人提过使用基于调度的线程池ScheduledExecutorService来实现,里面提三种维度的方法实现

1、schedule。单次延迟任务。

2、scheduleAtFixedRate。基于固定时间间隔进行循环延迟任务。如果上一次任务还没有结束,会等它结束后,才执行下一次任务,取间隔时间和任务执行时间的最大值。

3、scheduleWithFixedDelay。取决于每次任务执行的时间长短,是基于不固定时间间隔进行循环延迟任务,每次执行时间为上一次任务结束起向后推一个时间间隔,即每次执行时间为:initialDelay, initialDelay+executeTime+delay, initialDelay+2executeTime+2delay

点评:

1、上面提到的第二、三种,都是循环执行任务,区别在与执行的时间调度上有区别。不适合本文的业务场景

2、方法一,也就是schedule,属于单次执行,且时间支持灵活计算,本文业务场景的时间=(目标执行时间-当前时间)

MyScheduledRunnable runnable = new MyScheduledRunnable();// 业务运行2秒runnable.setBizCostTime(2000L);ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();// 单次任务,延迟1秒,开始执行任务service.schedule(runnable, 1, TimeUnit.SECONDS);

二、数据库轮询扫描

按业务需求,我们会创建一个超时记录表,当业务执行时会插入一条记录到mysql表,并指定目标执行时间。

字段名

字段类型

描述

id

number

主键

head

varchar2(1024)

业务参数

type

varchar2(100)

类型

status

varchar2(100)

状态

gmt_time

date

目标执行时间

然后会启动一个定时任务,一般会采用 Quartz 框架来实现, 无限循环扫描该表记录,如果发现目标执行时间小于当前时间,会提取记录执行并修改状态。为什么要先修改状态呢?主要是考虑多线程并发问题,毕竟执行超时任务(如:自动确认收货)也要花费时间,待超时任务执行结束后,再修改状态标记为“已完成”。

缺点:采用主动发现机制,执行时间严重依赖扫描频率,如果定时任务配置的时间周期太长,那么任务真正执行时间可能会有较大延迟。反之,如果扫描周期时间太短,扫描频率过快,数据库的压力会比较大,还存在较大的系统资源浪费。

如果表的数据量过大,每次扫描任务负担会很重,我们会考虑采用分库分表机制,每张物理表都有独立的扫描线程,提高处理速度。另外,当任务已经执行完成,该记录基本没什么业务价值,会有归档任务,对历史数据定期清理。

优点: 实现简单、无技术难点、异常恢复、支持分布式/集群环境

三、Redis 有序集合实现延迟任务

Redis提供了丰富的数据存储结构,其中Zset支持按score对value值排序,这里的score可以采用超时记录的目标执行时间。也就是说集合列表中的记录是按执行时间排好序,我们只需要取小于当前时间的即可。

为了避免一次拉取的记录过多,导致程序处理压力过大,在调用 redisTemplate.opsForZSet().rangeByScoreWithScores(key, 0, max, 0, count);   一般我们会限制拉取的条数,比如一次只拉取最小的50条,降低单次处理的RT时长。

@Testpublic void test() throws InterruptedException {    //清理数据    cacheService.delKey(keyPrefix);    // 模拟插入10条超时记录    for (int i = 1; i <= 10; i++) {        long delayTime = Instant.now().plusSeconds(i + 4).getEpochSecond();        boolean result = cacheService.addData(keyPrefix, "v" + i, delayTime);        if (result) {            System.out.println("记录:" + i + " 插入成功!");        }    }    // 启动延迟队列扫描    while (true) {        long nowtTime = Instant.now().getEpochSecond();        // 一次扫描出小于当前时间且按时间排序的最小两条记录        List<String> result = cacheService.scanData(keyPrefix, nowtTime, 3);        if (result != null) {            for (String record : result) {                // 对ZREM的返回值进行判断,只有大于0的时候,才消费数据                // 防止多个线程消费同一个资源的情况                long affectRow = cacheService.removeData(keyPrefix, record);                if (affectRow > 0) {                    // 模拟业务处理                    System.out.println("处理超时记录:" + record);                }            }        }        Thread.sleep(800);    }}

优点: 解耦、异常恢复、支持分布式/集群环境;

四、pulsa 消息实现延迟任务

当前公司使用 pulsa 消息中间件,我们来看下如何借助现成的消息框架来实现延迟任务。

当producer发出一个延迟消息,订阅方并不会立即收到消息,消息存储在BookKeeper中,DelayedDeliveryTracker将时间索引(time-> messageId)保存在内存中,一旦延迟时间到了,消息会被发布到一台broker,然后传递给订阅者。

延迟消息传递仅在共享订阅模式下有效。在“独占”和“故障转移”订阅模式下,延迟的消息会立即分派。

代码示例:

producer.newMessage().deliverAfter(3L, TimeUnit.Minute).value("Hello Pulsar!").send();

更多还是借助于pulsa消息框架本身机制来实现功能,你会发现调用的API非常简单。

五、ActiveMQ 消息实现延迟任务

ActiveMQ作为一个开箱即用的中间件,提供了扩展配置属性支持延迟消息。

示例1:延迟60秒发送消息​​​​​​​

MessageProducer producer = session.createProducer(destination);TextMessage message = session.createTextMessage("test msg");long time = 60 * 1000;message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, time);producer.send(message);

示例2:开始延迟30秒发送,重复发送10次,每次之间间隔10秒​​​​​​​

MessageProducer producer = session.createProducer(destination);TextMessage message = session.createTextMessage("test msg");long delay = 30 * 1000;long period = 10 * 1000;int repeat = 9;message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, delay);message.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD, period);message.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT, repeat);producer.send(message);

示例3:使用Cron 表示式定时发送消息​​​​​​​

MessageProducer producer = session.createProducer(destination);TextMessage message = session.createTextMessage("test msg");message.setStringProperty(ScheduledMessage.AMQ_SCHEDULED_CRON, "0 * * * *");producer.send(message);

六、Netty 实现延迟任务

由于netty动辄管理10w+的连接,每一个连接都会有很多超时任务。比如发送超时、心跳检测间隔等,如果每一个定时任务都启动一个Timer,不仅低效,而且会消耗大量的资源。

时间轮是一种高效来利用线程资源来进行批量化调度的一种调度模型。把大批量的调度任务全部都绑定到同一个的调度器上面,使用这一个调度器来进行所有任务的管理(manager),触发(trigger)以及运行(runnable)。能够高效的管理各种延时任务,周期任务,通知任务等等。

缺点,时间轮调度器的时间精度可能不是很高,对于精度要求特别高的调度任务可能不太适合。因为时间轮算法的精度取决于,时间段“指针”单元的最小粒度大小,比如时间轮的格子是一秒跳一次,那么调度精度小于一秒的任务就无法被时间轮所调度。而且时间轮算法没有做宕机备份,因此无法再宕机之后恢复任务重新调度。

​​​​​​​

// 初始化netty时间轮HashedWheelTimer timer = new HashedWheelTimer(1, // 时间间隔        TimeUnit.SECONDS,        10); // 时间轮中的槽数TimerTask task1 = new TimerTask() {    @Override    public void run(Timeout timeout) throws Exception {        System.out.println("已经过了" + costTime() + " 秒,task1 开始执行");    }};TimerTask task2 = new TimerTask() {    @Override    public void run(Timeout timeout) throws Exception {        System.out.println("已经过了" + costTime() + " 秒,task2 开始执行");    }};TimerTask task3 = new TimerTask() {    @Override    public void run(Timeout timeout) throws Exception {        System.out.println("已经过了" + costTime() + " 秒,task3 开始执行");    }};// 将任务添加到延迟队列timer.newTimeout(task1, 0, TimeUnit.SECONDS);timer.newTimeout(task2, 3, TimeUnit.SECONDS);timer.newTimeout(task3, 15, TimeUnit.SECONDS);

写在最后

附上本文示例代码:

https://github.com/aalansehaiyang/project-example

往期推荐

  • 深入剖析优惠券核心架构设计

  • 某生鲜电商平台的库存扣减方案

  • 被吓了一跳,算一算优惠券的利润账!

  • 总监路上的第1年,聊聊几点感受

  • 如何玩好优惠券这把营销利剑?

  • Spring Boot 集成 Elasticsearch 实战

  • 如何设计一个高性能的秒杀系统

  • 如何通过Binlog来实现不同系统间数据同步

  • 电商优惠券如何设计?

  • 单台 MySQL 支撑不了这么多的并发请求,我们该怎么办?

  • Github点赞接近100k的SpringBoot学习教程+实战推荐!牛批!

  • 如何打造一个高效的研发团队

  • 聊聊电商促销业务

  • DDD是如何解决复杂业务扩展问题?

  • 线上服务的FGC问题排查,看这篇就够了!

  • springboot + aop + Lua分布式限流的最佳实践

我们热衷于收集&分享高并发、系统架构、微服务、消息中间件、 RPC框架、高性能缓存、搜索、分布式数据框架、分布式协同服务、分布式配置中心、中台架构、领域驱动设计、系统监控、系统稳定性等技术知识

淘宝订单自动确认收货的N种实现,秒杀面试官相关推荐

  1. php 自动收货’_PHP实现电商订单自动确认收货redis队列

    一.场景 之前做的电商平台,用户在收到货之后,大部分都不会主动的点击确认收货,导致给商家结款的时候,商家各种投诉,于是就根据需求,要做一个订单在发货之后的x天自动确认收货.所谓的订单自动确认收货,就是 ...

  2. PHP与redis队列实现电商订单自动确认收货

    一.场景 之前做的电商平台,用户在收到货之后,大部分都不会主动的点击确认收货,导致给商家结款的时候,商家各种投诉,于是就根据需求,要做一个订单在发货之后的x天自动确认收货.所谓的订单自动确认收货,就是 ...

  3. php自动收货,如何在PHP中实现一个订单自动确认收货的redis队列

    如何在PHP中实现一个订单自动确认收货的redis队列 发布时间:2020-12-14 15:45:55 来源:亿速云 阅读:65 作者:Leah 本篇文章为大家展示了如何在PHP中实现一个订单自动确 ...

  4. php 订单自动确认收货,recieve.php判断不严谨导致邮件自动确认收货问题修正

    ecshop商家用户发现后台点击发货时会主动发送邮件到用户预留的联络邮箱中, 简直国外的网站都是这么操作的,所以客户也选用此种方法来告诉用户. 但不料没多久,但是不少买家反映自己分明没有点击邮件中的承 ...

  5. 自动取消订单/自动确认收货

    1.定时任务 2.JDK的延迟队列 3.HashedWheelTimer 4.redis缓存 5.消息队列

  6. 从零开发短视频电商 30分钟未支付订单自动关闭、七天自动确认收货等延迟任务问题

    文章目录 常见延迟任务 常见解决方案 主动形式 被动形式 基于Redis实现ZSet的方式.键空间通知的方式 ZSet的方式 键空间通知的方式 RocketMQ延迟消息 延迟消息级别配置 客户端发送延 ...

  7. 艾司博讯:拼多多没确认收货多久系统才自动确认收货

    现在大多朋友对网购都不陌生,一般我们收到货后都会确认收货,然后给商品进行评价.当然,一些朋友比较忙或者没有确认收货的习惯,过了一段时间后会自动确认收货.那么拼多多确认收货期限是几天,下面就为大家带来介 ...

  8. php怎么点击确认收货,解决修正Ecshop的recieve.php邮件自动确认收货问题

    解决修正Ecshop的recieve.php邮件自动确认收货问题 将之前的ecshop确认收货页面改成如下格式: //确认页面 $act = !empty($_REQUEST['act']) ? ra ...

  9. 拼多多自动确认收货后还能退吗?退货申请期限是多久?

    其实,在确认收货一点也不难,只需要签收后,点击确认收货按钮就行了.不过,还是有不少伙伴都了懒于去点击确认收货,这个时候系统会自动确认收货 拼多多自动确认收货后还能退吗? 1.常规商品15天内是消费者收 ...

  10. php 10天自动确认收货,ECSHOP修正recieve.php判断不严谨导致邮件自动确认收货

    今天小编在一个技术论坛上看了这样一个求助帖,一位用ecshop建站的店长朋友反应"用户明明没有确认收货,可是会员管理里面,订单状态却显示为已经确认收货".看到这个求助帖之后,小编很 ...

最新文章

  1. O - Layout POJ - 3169(差分约束)
  2. 艾伟_转载:把事件当作对象进行传递
  3. eclipse C/C++环境搭建
  4. 手游特效太多怎么办?这里有一份性能优化方案可参考
  5. pdf.js插件使用记录,在线打开pdf
  6. 品质背景壁纸网站高图网,选图不用瞎找了!
  7. STL(1)——查找函数find的使用
  8. ajax常见写法,jquery ajax较常见的写法
  9. NLTK学习笔记(六):利用机器学习进行文本分类
  10. 服务器系统更新失败进不了系统,第五人格更新后进不去怎么办 更新连接服务器失败...
  11. 基于mysql学生成绩管理论文_基于SQL Server的学生成绩管理系统设计论文
  12. 对于火灾和火焰检测的初步学习
  13. 2008r2配置 iis mysql php_Windows Server 2008 R2 IIS7+PHP5(FastCGI)+MySQL5环境搭建
  14. 设置背景图片大小的方法
  15. 【k8s】kubernetes编写自己的operator(operator-sdk:v1.xxx)
  16. 无线路由器服务器连接线,无线路由器连接有线路由器怎么设置?
  17. Angel 相关学习
  18. Python开源项目合集(网页框架)
  19. [FirefoxOS_开发环境]Linux和Ubuntu环境下B2G(Firefox OS)安装、编译、测试教程集合
  20. python和scre_python中变量命名的基本规则,标识符和关键字

热门文章

  1. 快速实现大量数据匹配来电号码归属
  2. sql语句中select……as的用法
  3. nodejs串口通信
  4. html上绑定回车事件,js/jquery中input 绑定回车enter事件的代码
  5. 蓝桥杯之《人民币金额大写》
  6. js-通过audioContext实现3D音效
  7. Qt+OSG/osgEarth跨平台编译(用Qt Creator组装各个库,实现一套代码、一套框架,跨平台编译)
  8. 5 steps to autotools GNU diction
  9. php包含那点事情[WOOYUN]
  10. 全球首秀!真人数字人亮相元宇宙签约仪式