阿里面试官问:生成订单30分钟未支付,则自动取消,该怎么实现?

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
img

正文

思路

该方案是利用JDK自带的DelayQueue来实现,这是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素,放入DelayQueue中的对象,是必须实现Delayed接口的。

DelayedQueue实现工作流程如下图所示

图片

其中Poll():获取并移除队列的超时元素,没有则返回空

take():获取并移除队列的超时元素,如果没有则wait当前线程,直到有元素满足超时条件,返回结果。

实现

定义一个类OrderDelay实现Delayed,代码如下​​​​​​​

package com.rjzheng.delay2; import java.util.concurrent.Delayed;import java.util.concurrent.TimeUnit;` `public class OrderDelay implements Delayed {` ` private String orderId;` `private long timeout;` ` OrderDelay(String orderId, long timeout) {` `this.orderId = orderId;` `this.timeout = timeout + System.nanoTime();` `}` ` public int compareTo(Delayed other) {` `if (other == this)` `return 0;` `OrderDelay t = (OrderDelay) other;` `long 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);` `}` ` void print() {` `System.out.println(orderId+"编号的订单要删除啦。。。。");` `}}

运行的测试Demo为,我们设定延迟时间为3秒​​​​​​​

package com.rjzheng.delay2; import java.util.ArrayList;import java.util.List;import java.util.concurrent.DelayQueue;import java.util.concurrent.TimeUnit;` `public class DelayQueueDemo {` `public static void main(String[] args) { // TODO Auto-generated method stub List<String> list = new ArrayList<String>(); list.add(“00000001”); list.add("00000002"); list.add(“00000003”); list.add("00000004"); list.add(“00000005”); DelayQueue<OrderDelay> queue = newDelayQueue<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("After " + (System.currentTimeMillis()-start) + " MilliSeconds"); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }

输出如下​​​

00000001编号的订单要删除啦。。。。After 3003 MilliSeconds00000002编号的订单要删除啦。。。。After 6006 MilliSeconds00000003编号的订单要删除啦。。。。After 9006 MilliSeconds00000004编号的订单要删除啦。。。。After 12008 MilliSeconds00000005编号的订单要删除啦。。。。``After 15009 MilliSeconds

可以看到都是延迟3秒,订单被删除

优缺点

优点:效率高,任务触发时间延迟低。

缺点:

(1)服务器重启后,数据全部消失,怕宕机

(2)集群扩展相当麻烦

(3)因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常

(4)代码复杂度较高

(3)时间轮算法

思路

先上一张时间轮的图(这图到处都是啦)

图片

时间轮算法可以类比于时钟,如上图箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个 tick。这样可以看出定时轮由个3个重要的属性参数,ticksPerWheel(一轮的tick数),tickDuration(一个tick的持续时间)以及 timeUnit(时间单位),例如当ticksPerWheel=60,tickDuration=1,timeUnit=秒,这就和现实中的始终的秒针走动完全类似了。

如果当前指针指在1上面,我有一个任务需要4秒以后执行,那么这个执行的线程回调或者消息将会被放在5上。那如果需要在20秒之后执行怎么办,由于这个环形结构槽数只到8,如果要20秒,指针需要多转2圈。位置是在2圈之后的5上面(20 % 8 + 1)

实现

我们用Netty的HashedWheelTimer来实现

给Pom加上下面的依赖​​​​​​​

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

测试代码HashedWheelTimerTest如下所示​​​​​​​

package com.rjzheng.delay3; import io.netty.util.HashedWheelTimer;import io.netty.util.Timeout;import io.netty.util.Timer;import io.netty.util.TimerTask;` `import java.util.concurrent.TimeUnit;` `public class HashedWheelTimerTest {` `static class MyTimerTask implements TimerTask{` `boolean flag;` `public MyTimerTask(boolean flag){` `this.flag = flag;` `}` `public void run(Timeout timeout) throws Exception {` `// TODO Auto-generated method stub` `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 {` `Thread.sleep(1000);` `} catch (InterruptedException e) {` `// TODO Auto-generated catch block` `e.printStackTrace();` `}` `System.out.println(i+"秒过去了");` `i++;` `}` `}}

输出如下​​​​​​​

1秒过去了2秒过去了3秒过去了4秒过去了5秒过去了要去数据库删除订单了。。。。6秒过去了

优缺点

优点:效率高,任务触发时间延迟时间比delayQueue低,代码复杂度比delayQueue低。

缺点:

(1)服务器重启后,数据全部消失,怕宕机

(2)集群扩展相当麻烦

(3)因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常

(4)redis缓存

思路一

利用redis的zset,zset是一个有序集合,每一个元素(member)都关联了一个score,通过score排序来取集合中的值

zset常用命令

  • 添加元素:ZADD key score member [[score member] [score member] …]

  • 按顺序查询元素:ZRANGE key start stop [WITHSCORES]

  • 查询元素score:ZSCORE key member

  • 移除元素:ZREM key member [member …]

测试如下​​​​​​​

添加单个元素 redis> ZADD page_rank 10 google.com(integer) 1` `# 添加多个元素` `redis> ZADD page_rank 9 baidu.com 8 bing.com(integer) 2 redis> ZRANGE page_rank 0 -1 WITHSCORES1) "bing.com"2) "8"3) "baidu.com"4) “9"5) "google.com"6) “10” # 查询元素的score值redis> ZSCORE page_rank bing.com"8” # 移除单个元素 redis> ZREM page_rank google.com(integer) 1` `redis> ZRANGE page_rank 0 -1 WITHSCORES1) "bing.com"2) "8"3) “baidu.com”``4) “9”

那么如何实现呢?我们将订单超时时间戳与订单号分别设置为score和member,系统扫描第一个元素判断是否超时,具体如下图所示

图片

实现一​​​​​​​

package com.rjzheng.delay4; import java.util.Calendar;import java.util.Set;` `import redis.clients.jedis.Jedis;import redis.clients.jedis.JedisPool;``import redis.clients.jedis.Tuple; public class AppTest { private static final String ADDR = “127.0.0.1”; private static final int PORT = 6379; private static JedisPool jedisPool = new JedisPool(ADDR, PORT); public static Jedis getJedis() { return jedisPool.getResource(); } //生产者,生成5个订单放进去 public void productionDelayMessage(){ for(int i=0;i<5;i++){ //延迟3秒 Calendar cal1 = Calendar.getInstance(); cal1.add(Calendar.SECOND, 3); int second3later = (int) (cal1.getTimeInMillis() / 1000); AppTest.getJedis().zadd(“OrderId”,second3later,“OID0000001”+i); System.out.println(System.currentTimeMillis()+“ms:redis生成了一个订单任务:订单ID为”+“OID0000001”+i); } } //消费者,取订单 public void consumerDelayMessage(){ Jedis jedis = AppTest.getJedis(); while(true){ Set items = jedis.zrangeWithScores(“OrderId”, 0, 1); if(items == null || items.isEmpty()){ System.out.println(“当前没有等待的任务”); try { Thread.sleep(500); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } continue; } int score = (int) ((Tuple)items.toArray()[0]).getScore(); Calendar cal = Calendar.getInstance(); int nowSecond = (int) (cal.getTimeInMillis() / 1000); if(nowSecond >= score){ String orderId = ((Tuple)items.toArray()[0]).getElement(); jedis.zrem(“OrderId”, orderId); System.out.println(System.currentTimeMillis() +“ms:redis消费了一个任务:消费的订单OrderId为”+orderId); } } } public static void main(String[] args) { AppTest appTest =new AppTest(); appTest.productionDelayMessage(); appTest.consumerDelayMessage(); } }

此时对应输出如下

图片

可以看到,几乎都是3秒之后,消费订单。

然而,这一版存在一个致命的硬伤,在高并发条件下,多消费者会取到同一个订单号,我们上测试代码ThreadTest

package com.rjzheng.delay4; import java.util.concurrent.CountDownLatch; public class ThreadTest { private static final int threadNum = 10; private static CountDownLatch cdl = newCountDownLatch(threadNum); static class DelayMessage implements Runnable{ public void run() { try { cdl.await(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } AppTest appTest =new AppTest(); appTest.consumerDelayMessage(); } } public static void main(String[] args) { AppTest appTest =new AppTest(); appTest.productionDelayMessage(); for(int i=0;i<threadNum;i++){ new Thread(new DelayMessage()).start(); cdl.countDown(); } }``}

输出如下所示

图片

显然,出现了多个线程消费同一个资源的情况。

解决方案

(1)用分布式锁,但是用分布式锁,性能下降了,该方案不细说。

(2)对ZREM的返回值进行判断,只有大于0的时候,才消费数据,于是将consumerDelayMessage()方法里的​​​​​​​

if(nowSecond >= score){ String orderId = ((Tuple)items.toArray()[0]).getElement(); jedis.zrem(“OrderId”, orderId); System.out.println(System.currentTimeMillis()+“ms:redis消费了一个任务:消费的订单OrderId为”+orderId);``}

修改为

if(nowSecond >= score){ String orderId = ((Tuple)items.toArray()[0]).getElement(); Long num = jedis.zrem(“OrderId”, orderId); if( num != null && num>0){ System.out.println(System.currentTimeMillis()+“ms:redis消费了一个任务:消费的订单OrderId为”+orderId); }``}

在这种修改后,重新运行ThreadTest类,发现输出正常了

思路二

该方案使用redis的Keyspace Notifications,中文翻译就是键空间机制,就是利用该机制可以在key失效之后,提供一个回调,实际上是redis会给客户端发送一个消息。是需要redis版本2.8以上。

实现二

在redis.conf中,加入一条配置

最后

按照上面的过程,4个月的时间刚刚好。当然Java的体系是很庞大的,还有很多更高级的技能需要掌握,但不要着急,这些完全可以放到以后工作中边用别学。

学习编程就是一个由混沌到有序的过程,所以你在学习过程中,如果一时碰到理解不了的知识点,大可不必沮丧,更不要气馁,这都是正常的不能再正常的事情了,不过是“人同此心,心同此理”的暂时而已。

道路是曲折的,前途是光明的!”

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
了,不过是“人同此心,心同此理”的暂时而已。

道路是曲折的,前途是光明的!”

[外链图片转存中…(img-6FIixqZH-1713555374310)]

[外链图片转存中…(img-qWstcGQF-1713555374311)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-jZlY3KZq-1713555374312)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 21
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值