平常我们都用过淘宝,京东这些电商平台,同时肯定也在这些平台上面下过单,这种情况不保证大家都有遇到过,但做开发的,肯定也知道有这个环节的存在:确认货品配置无误之后,我们都会点击购买,随之而来的就是一个结算页,让你确认商品信息、收货地址、价格等信息,但是如果这个时候,我们退出这个结算页,那此时,这个订单就属于生成但未支付的状态,对应的电商平台就会对这样的订单做延时任务取消操作,这样就可以稳定商品库存和提供用户一定的犹豫时间,那从技术的角度来看,这个延时任务是怎么实现的?


疑问1

看着上面的案例,有的人会问:这个简单,我们直接用定时任务,做个半小时后的查询数据库来判断是该取消了还是怎么样不就行了吗?

解答

其实这个场景,我们就不能说是定时任务了,准确来说是延时任务,那什么是延时呢?延时就是从一个点开始后的多久后再执行,比方说:火车八点到站,但是晚点十分钟,是那就是八点后的十分钟

疑问2

上面的解答,概念有点模棱两可,和定时任务不是特别能区分开,那接下来我们就介绍一下定时和延时任务的区别


定时和延时区别

1、定时任务有明确的触发事件,而延迟任务没有

举例:火车行程表
进入火车站,我们都会看到大屏上显示各车次的出发时间、始发地、终点站等等信息

定时任务:火车半小时一列,出发时间就是每天0点开始算,0:30一班,1:00一班,
哪怕第一趟火车0:10到站,那第二趟火车依旧是0:30到站

延时任务:第二趟火车是在第一趟火车到站之后的半小时后一班,比如第一班火车晚点了,
是0:10分到站,那第二趟火车就是0:40到站,由前面那趟火车到站时间为起点后的半小时作为第二趟火车的到站时间

2、定时任务有执行周期,而延时任务是在条件满足之后的一段时间后再执行,没有执行周期

举例:站岗换班

定时任务:晚上22点开始站岗,每半小时换一次人,那接下来就是22:30、23:00、23:30...没半小时换一个人,周期是固定的,半小时一换,硬性要求,半小时必换班

延时任务:同样是晚上22点开始站岗,每半小时换一次人,假设第一个人站岗已经两个小时了,需要第二个人来替班,这个时候第二个人才会来替班,第二个人替班的前提是:第一个人满足半小时站岗时间并且累了要求换人,而且第二个人替班时间是:0点,并非22:30,要求是半小时一换,但是有个前提,如果第一个人超过半小时而且不换,那就没有执行周期了,第二个人就可以摸鱼了

3、定时任务一般执行的都是多个任务,而延时任务一般来说都是单个任务

举例:订单取消

定时任务:如果用定时任务来取消订单,如果定死是30分钟整,不考虑秒的话,那可能到了半小时之后,会取消多个不同秒下单的订单,这个场景用代码来实现的话,最常见的就是把数据库订单的创建时间批量查出来,只精确到分,>30分钟的都要取消的,这取消的可能是大批量的数据和任务

延时任务:同样是半小时整,但因为上面1和2的条件,延时任务针对的是每笔订单下单未支付后的半小时内的订单取消,他自然而然会把秒考虑进去,比如,9:00:55这个时间下单未支付,那取消订单会在9:30:55取消,同时我9:01:02下了另一个单,但未支付,那取消这笔订单的时间就是9:31:02


那接下来我们正式来介绍一下有多少种常见的延时方案

数据库轮询

实现思路

一般来说会直接用定时框架quartz来实现,通过一个线程定时的去请求数据库,通过订单的创建时间计算出有没有超过规定时间没支付的订单,如果有就update,没有就等下一轮轮询

【优点】

相对简单,支持集成多线程

【缺点】

1、对服务器消耗比较大

2、可能会存在延迟

3、扫描的如果是订单这种大量数据的表,那频繁的请求数据库,数据库的压力也不小


JDK延迟队列

实现思路

利用JDK提供的DelayQueue队列来实现,它是一个无界阻塞队列,啥意思呢,就是说这个队列只有在延迟期满了之后才能从里面获取元素

代码Demo

1、定义订单OrderDelay来继承Delayed

public class OrderDelay implements Delayed {private String orderNo;private long timeout;OrderDelay(String orderNo, long timeout) {this.orderNo = orderNo;//规定超时的时间+订单创建时间(现在)时间戳this.timeout = timeout + System.nanoTime();}/*** @Description: 时间比对*/public int compareTo(Delayed other) {if (other == this) return 0;OrderDelay t = (OrderDelay) other;//设定的有效时间-当前系统时间是否是小于等于0long d = (getDelay(TimeUnit.NANOSECONDS) - t.getDelay(TimeUnit.NANOSECONDS));return (d == 0) ? 0 : ((d < 0) ? -1 : 1);}// 返回距离你自定义的超时时间还有多少public long getDelay(TimeUnit unit) {return unit.convert(timeout - System.nanoTime(), TimeUnit.NANOSECONDS);}public void print() {System.out.println("订单号为:" + orderNo + "的订单即将被取消支付");}
}

2、编写main方法

 public static void main(String[] args) {List<String> list = new ArrayList<String>();//模拟订单号list.add("DD111111");list.add("DD222222");list.add("DD333333");list.add("DD444444");list.add("DD555555");//存入队列DelayQueue<OrderDelay> queue = new DelayQueue<OrderDelay>();long start = System.currentTimeMillis();for (int i = 0; i < 5; i++) {//延迟三秒取消订单queue.put(new OrderDelay(list.get(i),TimeUnit.NANOSECONDS.convert(3, TimeUnit.SECONDS)));try {queue.take().print();System.out.println("删除时间:" +(System.currentTimeMillis() - start) + "s");System.out.println("=====================================================");} catch (InterruptedException e) {e.printStackTrace();}}}

3、观察控制台输出

订单号为:DD111111的订单即将被取消支付
删除时间:3005s
=====================================================
订单号为:DD222222的订单即将被取消支付
删除时间:6018s
=====================================================
订单号为:DD333333的订单即将被取消支付
删除时间:9027s
=====================================================
订单号为:DD444444的订单即将被取消支付
删除时间:12042s
=====================================================
订单号为:DD555555的订单即将被取消支付
删除时间:15058s
=====================================================

看时间其实是可以知道的,两个相邻的订单被取消的时间都在3秒,每3秒取消一笔订单

【优点】

效率高,延迟低

【缺点】

1、服务器重启或者宕机,数据会全部丢失

2、集群拓展困难

3、如果下单未付款的订单数太多,会出现OOM的异常

4、代码复杂度较高,上面的案例较为简单,可以实现一些指定的简单场景


时间轮-算法

实现逻辑

可以用时钟来理解,我们看一下较长的时针,时针按照某一个方向按固定的速度或频率转动,每一次刻度是一个tick,所以就引出了时间轮的三个概念:ticksPerWheel(一轮的tick数),tickDuration(一个tick的持续时间)和timeUnit(时间单位),怎么理解这三个概念呢?比如:如果ticksPerWheel=60,tickDuration=1,timeUnit=秒,那就和我们平常认知里的时钟的时针转动的速度是一样的了

结合案例说一下:如果指针指到2那个刻度,同时一个任务需要4秒以后执行,那么这个执行的线程回调会放在6上面,如果需要20s之后执行会咋样呢,由于这个始终只有12个,如果是20s,就转一圈再转一圈,20个刻度停止,也就是10点的那个位置

代码Demo

1、使用netty提供的HashedWheelTimer来实现。先添加Netty依赖

<dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.42.Final</version>
</dependency>

2、创建一个定时任务

public class HashedWheelTimerTest {static class MyTimerTask implements TimerTask {boolean flag;public MyTimerTask(boolean flag) {this.flag = flag;}//要执行延时的逻辑public void run(Timeout timeout) {System.out.println("正在取消订单的路上...");this.flag = false;}}public static void main(String[] argv) {MyTimerTask timerTask = new MyTimerTask(true);Timer timer = new HashedWheelTimer();//三个参数分别是,定时要实现的定时任务/延时时长/时间单位(这儿是秒)timer.newTimeout(timerTask,          5,     TimeUnit.SECONDS);int i = 1;while (timerTask.flag) {try {//延时1s,1s打印一次,上面的延时是五秒后Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("已经过去了" + i + "秒");i++;}}
}

3、Timer中的newTimeout讲解

public Timeout newTimeout(TimerTask task, long delay, TimeUnit unit) {if (task == null) {throw new NullPointerException("task");} else if (unit == null) {throw new NullPointerException("unit");} else {//任务数,相当于线程池的大小long pendingTimeoutsCount = this.pendingTimeouts.incrementAndGet();//不能超过最大任务数if (this.maxPendingTimeouts > 0L && pendingTimeoutsCount > this.maxPendingTimeouts) {this.pendingTimeouts.decrementAndGet();throw new RejectedExecutionException("Number of pending timeouts (" + pendingTimeoutsCount + ") is greater than or equal to maximum allowed pending timeouts (" + this.maxPendingTimeouts + ")");} else {//开始上面demo中的任务this.start();//计算是否超时long deadline = System.nanoTime() + unit.toNanos(delay) - this.startTime;if (delay > 0L && deadline < 0L) {deadline = 9223372036854775807L;}//添加到timeouts队列,这是一个MPSC队列,MPSC队列是多生产者单消费者无锁的并发队列,worker线程会对队列进行消费HashedWheelTimer.HashedWheelTimeout timeout = new HashedWheelTimer.HashedWheelTimeout(this, task, deadline);this.timeouts.add(timeout);return timeout;}}}

4、最后打印

已经过去了1秒
已经过去了2秒
已经过去了3秒
已经过去了4秒
已经过去了5秒
正在取消订单的路上...
已经过去了6秒

第五秒之后会去执行取消订单的操作

【优点】

1、效率比较高,整体来说比JDK提供的DeplayQueue效率要好

【缺点】

1、服务重启或宕机,数据会丢失

2、对集群拓展不太友好

3、和JDK一样,会出现OOM异常


Redis缓存

实现思路

使用Redis五大数据类型中的zset类型,它是一个有序集合,每一个元素都有一个score,通过这个score排序来获取集合中的值,就取消订单来说,我们将订单超时的时间戳与订单号分别存放到score和member,然后让系统扫描第一个元素是否超时,这个相对简单,不讲了,说一个更好玩的思路

代码Demo

1、编写代码

public class ZsetTest {//创建redis实例private static JedisPool jedisPool = new JedisPool("127.0.0.1", 6379);public static Jedis getJedis() {return jedisPool.getResource();}//生产者,生成5个订单放进去public void productionDelayMessage(){for(int i=0;i<5;i++){Calendar cal1 = Calendar.getInstance();//延迟3秒cal1.add(Calendar.SECOND, 3);int second3later = (int) (cal1.getTimeInMillis() / 1000);//                          //ket       score                   memberZsetTest.getJedis().zadd("OrderNo",second3later,"ORDER0000001"+i);System.out.println(System.currentTimeMillis()+"ms:Redis生成了一个订单任务:订单号为"+"ORDER0000001"+i);}}//消费者,获取订单public void consumerDelayMessage(){Jedis jedis = ZsetTest.getJedis();while(true){//扫描key下面所有数据Set<Tuple> items = jedis.zrangeWithScores("OrderNo", 0, 1);if(items == null || items.isEmpty()){System.out.println("当前没有需要等待执行的任务");try {//等待500msThread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}continue;}//获取scoreint  score = (int) ((Tuple)items.toArray()[0]).getScore();Calendar cal = Calendar.getInstance();//计算现在的时间戳int nowSecond = (int) (cal.getTimeInMillis() / 1000);//对比,如果创建的时间戳已经不再规定的时间范围内,就是过期了if(nowSecond >= score){String orderNo = ((Tuple)items.toArray()[0]).getElement();//移除订单jedis.zrem("OrderNo", orderNo);System.out.println(System.currentTimeMillis() +"ms:Redis消费了一个订单任务:消费的订单号为"+orderNo);}}}public static void main(String[] args) {ZsetTest zsetTest =new ZsetTest();//生成订单zsetTest.productionDelayMessage();//获取订单,并取消订单zsetTest.consumerDelayMessage();}}

2、打印结果

1650851642873ms:Redis生成了一个订单任务:订单号为ORDER00000010
1650851642875ms:Redis生成了一个订单任务:订单号为ORDER00000011
1650851642876ms:Redis生成了一个订单任务:订单号为ORDER00000012
1650851642877ms:Redis生成了一个订单任务:订单号为ORDER00000013
1650851642878ms:Redis生成了一个订单任务:订单号为ORDER00000014
1650851645000ms:Redis消费了一个订单任务:消费的订单号为ORDER00000010
1650851645001ms:Redis消费了一个订单任务:消费的订单号为ORDER00000011
1650851645001ms:Redis消费了一个订单任务:消费的订单号为ORDER00000012
1650851645001ms:Redis消费了一个订单任务:消费的订单号为ORDER00000013
1650851645002ms:Redis消费了一个订单任务:消费的订单号为ORDER00000014
当前没有需要等待执行的任务
当前没有需要等待执行的任务

3、我这里可能打印结果体验不出来接下来要说的问题,那就是多线程会消费同一个资源,那怎么解决嘞?方案就是:对zrem返回值进行判断,>0的时候消费数据,否则不消费,我们修改一下上面demo代码中取消订单的if判断中的逻辑,修改后的代码如下:

if(nowSecond >= score){String orderNo = ((Tuple)items.toArray()[0]).getElement();//移除订单Long no = jedis.zrem("OrderNo", orderNo);if(no!=null && no>0){System.out.println(System.currentTimeMillis() +"ms:Redis消费了一个订单任务:消费的订单号为"+orderNo);}
}

【优点】

1、集群拓展相当友好

2、时间准确度较高

3、数据不易丢失,如果服务出现重启或宕机,当服务启动的时候,会有重新处理数据的可能

【缺点】

需要对应的Redis维护的能力


RabbitMQ队列

实现原理

RabbitMQ自身是可以实现延迟队列,RabbitMQ可以针对Queue和Message设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为dead letter

代码Demo

具体的延时队列的使用介绍和代码,请移驾~到下面的链接,专门为这一个需求写的文章

【RabbitMQ】延迟队列_阿小冰的博客-CSDN博客【RabbitMQ】延迟队列https://blog.csdn.net/qq_38377525/article/details/124989700

【优点】

1、效率较高

2、有关RabbitMQ的横向拓展也十分友好

3、支持数据持久化

【缺点】

需要对RabbitMQ的安装配置和API要有一定的功底

【笔记】下单但未支付的订单倒计时自动取消逻辑实现相关推荐

  1. Java秒杀系统实战系列~RabbitMQ死信队列处理超时未支付的订单(转)

    转自: https://juejin.cn/post/6844903903130042376 文末有源代码,非常棒 摘要: 本篇博文是"Java秒杀系统实战系列文章"的第十篇,本篇 ...

  2. Java秒杀系统实战系列~定时任务补充处理超时未支付的订单

    摘要: 本篇博文是"Java秒杀系统实战系列文章"的第十一篇,本篇博文我们将借助定时任务调度组件来辅助"失效超时未支付的订单记录"的处理,用以解决上篇博文中采用 ...

  3. 订单超时自动取消3种方案——我们用这种!

    大家好,大家对电商购物应该都比较熟悉了,我们应该注意到,在下单之后,通常会有一个倒计时,如果超过支付时间,订单就会被自动取消. 下单 今天,我们来聊聊订单超时未支付自动取消的几种方案. 1.定时任务 ...

  4. java订单到期自动取消_订单自动过期实现方案

    需求分析: 24小时内未支付的订单过期失效. 解决方案 被动设置:在查询订单的时候检查是否过期并设置过期状态. 定时调度:定时器定时查询并过期需要过期的订单. 延时队列:将未支付的订单放入一个延时队列 ...

  5. delayQueue实现订单超时自动取消

    目录 说明 实现 1.编写Delayed实现类 2.编写DelayQueue业务类 3.编写订单业务逻辑 总结说明 说明 商城系统的订单模块都应该有:订单未支付超时后自动取消订单的操作.我们在开发过程 ...

  6. SpringBoot项目中遇到的订单支付超时未支付关闭订单的解决方案

    1.扫表轮循 定时任务 => 获取数据 => 数据层 => 筛选出过期的数据 => 批量关闭超时订单 优点:实现简单.适用于小项目.数据量比较少 缺点:订单量过大的时候查询和修 ...

  7. 订单过期 自动取消实现方案

    在电商.支付等领域,往往会有这样的场景,用户下单后放弃支付了,那这笔订单会在指定的时间段后进行关闭操作,细心的你一定发现了像某宝.某东都有这样的逻辑,而且时间很准确,误差在1s内:那他们是怎么实现的呢 ...

  8. 使用DelayQueue模拟订单超时自动取消

    1.创建能在DelayQueue中存放的Order对象 package com.example.javastudy.delay_queue;import java.time.Duration; imp ...

  9. Java 微信支付接口(统一下单,异步回调,订单退款,取消订单)

    一.准备工作 APP绑定微信商户平台获取商户id(mchID).证书(商户后台下载).支付签名密钥(商户后台设置api密钥).退款签名密钥(商户后台设置api密钥ipv3)等 1.导入微信支付SDK ...

  10. 实现淘宝订单(比如订单生成、未支付的订单等等)自定义view的实现

    这段时间做的三个app会经常遇到自定义listview进行动态的添加商品然后进行结算,在加入到购物车进行结算,结算的方式就是老三样.首先实现的思路就是在主视图(比如fragment.activity或 ...

最新文章

  1. 2022-2028年中国动力电池回收行业投资分析及前景预测报告
  2. Paper8:Spatial Pyramid Pooling in Deep Convolutional Networks for Visual Recognition
  3. java map可以直接用增强for吗
  4. spring boot----简单入门
  5. centos 7 firefox启用java_一文详解各种花里胡哨的Java调试技巧,多图预警,记得收藏...
  6. VCL组件之公用对话框组件
  7. 1619. [HEOI2012]采花
  8. 用Blink打造你的技术朋友圈
  9. 社区发现(二)--GN
  10. 多处理器系统下的伪共享(false sharing)问题
  11. 多智能体协同视觉SLAM技术研究进展
  12. android 拷贝大文件,不用数据线,手机和电脑互传大文件
  13. Docker 映射端口telnet不通
  14. Microsoft Azure第一步——试用帐户申请
  15. python字母移位_python字母移位,凯撒密码
  16. SpringCloud之Eureka客户端服务启动报Cannot execute request on any known server解决
  17. 1. Vue CLI脚手架
  18. 请插入多卷集的最后一张磁盘,然后单击”确认“继续
  19. Cisco 3945路由器密码恢复,rommon模式操作详解
  20. 未明学院学员报告:喜马拉雅APP上,原来大家最爱听的是……

热门文章

  1. windows进程详解
  2. 浅述Docker的容器编排
  3. Python爬虫浏览器标识库
  4. plsql窗口文件怎么找回_简单粗暴搞定网易云音乐限制!找回失去的灰色快乐
  5. java实现录屏_java录屏详细代码
  6. 戴尔win10新电脑装linux,如何给戴尔新机装系统win10
  7. 多传感器融合的四种经典结构
  8. 如何优雅使用JDK8中的Stream对list集合中的某值求和
  9. 汉洛塔问题的递归解决方法
  10. 开口式霍尔电流传感器在数据中心直流配电改造的应用