网易首页 > 网易号 > 正文 申请入驻

拼夕夕订单超时未支付自动关闭实现方案!

0
分享至

  在开发中,往往会遇到一些关于延时任务的需求。例如:生成订单 30 分钟未支付,则自动取消;生成订单 60 秒后,给用户发短信。

  图片来自 Pexels

  对上述的任务,我们给一个专业的名字来形容,那就是延时任务。那么这里就会产生一个问题,这个延时任务和定时任务的区别究竟在哪里呢?

  一共有如下几点区别:

  定时任务有明确的触发时间,延时任务没有

  定时任务有执行周期,而延时任务在某事件触发后一段时间内执行,没有执行周期

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

  下面,我们以判断订单是否超时为例,进行方案分析。

  方案分析

  ①数据库轮询

  思路:该方案通常是在小型项目中使用,即通过一个线程定时的去扫描数据库,通过订单时间来判断是否有超时的订单,然后进行 update 或 delete 等操作。

  实现:博主当年早期是用 quartz 来实现的(实习那会的事),简单介绍一下。
maven 项目引入一个依赖,如下所示:

  org.quartz-schedulergroupId>

  quartzartifactId>

  2.2.2version>

  dependency>

  调用 Demo 类 MyJob,如下所示:

  package com.rjzheng.delay1;

  import org.quartz.JobBuilder;

  import org.quartz.JobDetail;

  import org.quartz.Scheduler;

  import org.quartz.SchedulerException;

  import org.quartz.SchedulerFactory;

  import org.quartz.SimpleScheduleBuilder;

  import org.quartz.Trigger;

  import org.quartz.TriggerBuilder;

  import org.quartz.impl.StdSchedulerFactory;

  import org.quartz.Job;

  import org.quartz.JobExecutionContext;

  import org.quartz.JobExecutionException;

  public class MyJob implements Job {

  public void execute(JobExecutionContext context)

  throws JobExecutionException {

  System.out.println("要去数据库扫描啦。。。");

  }

  public static void main(String[] args) throws Exception {

  // 创建任务

  JobDetail jobDetail = JobBuilder.newJob(MyJob.class)

  .withIdentity("job1", "group1").build();

  // 创建触发器 每3秒钟执行一次

  Trigger trigger = TriggerBuilder

  .newTrigger()

  .withIdentity("trigger1", "group3")

  .withSchedule(

  SimpleScheduleBuilder.simpleSchedule()

  .withIntervalInSeconds(3).repeatForever())

  .build();

  Scheduler scheduler = new StdSchedulerFactory().getScheduler();

  // 将任务及其触发器放入调度器

  scheduler.scheduleJob(jobDetail, trigger);

  // 调度器开始调度任务

  scheduler.start();

  }

  }

  运行代码,可发现每隔 3 秒,输出如下: 要去数据库扫描啦!

  优缺点:

  优点:简单易行,支持集群操作

  缺点:对服务器内存消耗大;存在延迟,比如你每隔 3 分钟扫描一次,那最坏的延迟时间就是 3 分钟;假设你的订单有几千万条,每隔几分钟这样扫描一次,数据库损耗极大。

  ②JDK 的延迟队列

  思路:该方案是利用 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 list = new ArrayList ();

  list.add( "00000001");

  list.add( "00000002");

  list.add( "00000003");

  list.add( "00000004");

  list.add( "00000005");

  DelayQueue queue = newDelayQueue ();

  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 MilliSeconds

  00000002编号的订单要删除啦。。。。

  After 6006 MilliSeconds

  00000003编号的订单要删除啦。。。。

  After 9006 MilliSeconds

  00000004编号的订单要删除啦。。。。

  After 12008 MilliSeconds

  00000005编号的订单要删除啦。。。。

  After 15009 MilliSeconds

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

  优缺点:

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

  缺点:服务器重启后,数据全部消失,怕宕机;集群扩展相当麻烦;因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现 OOM 异常;代码复杂度较高。

  ③时间轮算法

  思路:先上一张时间轮的图。

  时间轮算法可以类比于时钟,如上图箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个 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 加上下面的依赖:

  io.nettygroupId>

  netty-allartifactId>

  4.1.24.Finalversion>

  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 低。

  缺点:服务器重启后,数据全部消失,怕宕机;集群扩展相当麻烦;因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现 OOM 异常。

  ④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 WITHSCORES

  1) "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 WITHSCORES

  1) "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
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 中,加入一条配置:

  notify-keyspace-events Ex

  运行代码如下:

  package com.rjzheng.delay5;

  import redis.clients.jedis.Jedis;

  import redis.clients.jedis.JedisPool;

  import redis.clients.jedis.JedisPubSub;

  public class RedisTest {

  private static final String ADDR = "127.0.0.1";

  private static final int PORT = 6379;

  private static JedisPool jedis = new JedisPool(ADDR, PORT);

  private static RedisSub sub = new RedisSub();

  public static void init() {

  new Thread(new Runnable() {

  public void run() {

  jedis.getResource().subscribe(sub, "__keyevent@0__:expired");

  }

  }).start();

  }

  public static void main(String[] args) throws InterruptedException {

  init();

  for(int i =0;i<10;i++){

  String orderId = "OID000000"+i;

  jedis.getResource().setex(orderId, 3, orderId);

  System.out.println(System.currentTimeMillis()+"ms:"+orderId+"订单生成");

  }

  }

  static class RedisSub extends JedisPubSub {

  'http://www.jobbole.com/members/wx610506454'>@Override

  public void onMessage(String channel, String message) {

  System.out.println(System.currentTimeMillis()+"ms:"+message+"取消订单");

  }

  }

  }

  输出如下:

  可以明显看到 3 秒过后,订单取消了。PS:Redis 的 pub/sub 机制存在一个硬伤,官网内容如下:

  原:Because Redis Pub/Sub is fire and forget currently there is no way to use this feature if your application demands reliable notification of events, that is, if your Pub/Sub client disconnects, and reconnects later, all the events delivered during the time the client was disconnected are lost.

  
翻:Redis 的发布/订阅目前是即发即弃(fire and forget)模式的,因此无法实现事件的可靠通知。也就是说,如果发布/订阅的客户端断链之后又重连,则在客户端断链期间的所有事件都丢失了。因此,方案二不是太推荐。当然,如果你对可靠性要求不高,也可以使用。

  优缺点:

  优点:由于使用 Redis 作为消息通道,消息都存储在 Redis 中。如果发送程序或者任务处理程序挂了,重启之后,还有重新处理数据的可能性;做集群扩展相当方便;时间准确度高。

  缺点:需要额外进行 Redis 维护。

  ⑤使用消息队列

  我们可以采用 RabbitMQ 的延时队列。RabbitMQ 具有以下两个特性,可以实现延迟队列。

  RabbitMQ 可以针对 Queue 和 Message 设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为 dead letter。

  lRabbitMQ的 Queue 可以配置 x-dead-letter-exchange 和 x-dead-letter-routing-key(可选)两个参数,用来控制队列内出现了 deadletter,则按照这两个参数重新路由。

  结合以上两个特性,就可以模拟出延迟消息的功能。

  优缺点:

  优点:高效,可以利用 RabbitMQ 的分布式特性轻易的进行横向扩展,消息支持持久化增加了可靠性。

  缺点:本身的易用度要依赖于 RabbitMQ 的运维,因为要引用 RabbitMQ,所以复杂度和成本变高。

  作者:hjm4702192

  编辑:陶家龙

  出处:http://adkx.net/w73gf

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
里子面子都没了!赔光2亿后,妻子深夜亲手撕下邹市明最后的体面

里子面子都没了!赔光2亿后,妻子深夜亲手撕下邹市明最后的体面

离离言几许
2026-03-09 19:06:24
中东最后一套萨德系统被伊朗摧毁,现在最后悔的,估计是韩国!

中东最后一套萨德系统被伊朗摧毁,现在最后悔的,估计是韩国!

青青子衿
2026-03-09 19:47:45
伊朗称不再发射弹头不足一吨重导弹

伊朗称不再发射弹头不足一吨重导弹

财联社
2026-03-10 03:17:16
广东一醒狮表演高桩“失误”挂电线上,狮头脱落表演者错愕表情包走红!团队回应

广东一醒狮表演高桩“失误”挂电线上,狮头脱落表演者错愕表情包走红!团队回应

台州交通广播
2026-03-09 23:11:54
腾讯一天发布三款OpenClaw“龙虾”工具 可用微信“使唤”

腾讯一天发布三款OpenClaw“龙虾”工具 可用微信“使唤”

CNMO科技
2026-03-09 17:40:34
曝伊朗革命卫队全程陪同女足球员!球队尚未订机票 预计还有人逃跑

曝伊朗革命卫队全程陪同女足球员!球队尚未订机票 预计还有人逃跑

风过乡
2026-03-10 07:56:41
用亲人生命逼她们低头!伊朗女足的“国歌表演”,看哭了全世界

用亲人生命逼她们低头!伊朗女足的“国歌表演”,看哭了全世界

戗词夺理
2026-03-09 16:42:57
一堆人不知道!iPhone内置防晕车神器:网友实测超强大

一堆人不知道!iPhone内置防晕车神器:网友实测超强大

快科技
2026-03-08 18:15:04
专家:建议允许单身女性采用辅助生殖技术,这是对女性的尊重

专家:建议允许单身女性采用辅助生殖技术,这是对女性的尊重

映射生活的身影
2026-03-09 20:35:44
中东姑娘远嫁上海,三年后回家探亲痛哭流涕,坦白:实在是受不住

中东姑娘远嫁上海,三年后回家探亲痛哭流涕,坦白:实在是受不住

晨光苏醒a
2026-03-09 21:09:56
恐怖!深圳一男子被确诊艾滋病,传染他的,是在上高中的17岁男生

恐怖!深圳一男子被确诊艾滋病,传染他的,是在上高中的17岁男生

火山詩话
2026-03-09 09:07:09
打工人的天彻底塌了!养龙虾全网爆火,24小时干完6个人三周的活

打工人的天彻底塌了!养龙虾全网爆火,24小时干完6个人三周的活

乌娱子酱
2026-03-09 14:40:50
不服?朝鲜主帅抗议:主裁提前结束比赛不公!中国女足制胜球越位

不服?朝鲜主帅抗议:主裁提前结束比赛不公!中国女足制胜球越位

我爱英超
2026-03-09 21:28:15
震惊!男友父母花近1000万买新房,女子要加她的名字,然后退婚了

震惊!男友父母花近1000万买新房,女子要加她的名字,然后退婚了

火山詩话
2026-03-10 06:57:22
亚历山大35+9+15中制胜三分雷霆险胜掘金,约基奇32分14板13助

亚历山大35+9+15中制胜三分雷霆险胜掘金,约基奇32分14板13助

湖人崛起
2026-03-10 10:13:19
一张图看懂:穆杰塔巴有哪些亲属在美以袭击中丧生

一张图看懂:穆杰塔巴有哪些亲属在美以袭击中丧生

网易新闻出品
2026-03-09 22:07:03
王霜进球越位了?国际足联规则给出答案,朝鲜队难逃亚足联重罚

王霜进球越位了?国际足联规则给出答案,朝鲜队难逃亚足联重罚

何老师呀
2026-03-10 00:19:57
腾讯QClaw官网上线:可一键部署“龙虾”,兼容QQ、微信

腾讯QClaw官网上线:可一键部署“龙虾”,兼容QQ、微信

PChome电脑之家
2026-03-09 14:37:39
交通运输部约谈马士基集团和地中海航运公司

交通运输部约谈马士基集团和地中海航运公司

界面新闻
2026-03-10 10:21:01
上海这一夜,和孙俪挨着坐的肖战,让所有人见识了他的江湖地位

上海这一夜,和孙俪挨着坐的肖战,让所有人见识了他的江湖地位

八卦南风
2026-03-10 10:17:43
2026-03-10 11:55:00
IT架构师联盟 incentive-icons
IT架构师联盟
IT架构实战分享
836文章数 7671关注度
往期回顾 全部

科技要闻

“龙虾”狂欢 卖“饲料”先挣钱了?

头条要闻

业主举报违建次日接"邻居"电话:连我妈在哪打拳都知道

头条要闻

业主举报违建次日接"邻居"电话:连我妈在哪打拳都知道

体育要闻

韩国女足羡慕的奢侈品,为何选择中国女足

娱乐要闻

肖战首夺SMG视帝,孙俪四封视后创历史

财经要闻

全民"养龙虾"背后 第一批受害者浮现

汽车要闻

蔚来换电和理想5C,谁能硬刚,比亚迪兆瓦闪充?

态度原创

时尚
健康
数码
公开课
军事航空

爱马仕的暮色时分有多美?

转头就晕的耳石症,能开车上班吗?

数码要闻

小米推1199元米家洗衣机滚筒10kg新品:纯平一体设计,1.25洗净比

公开课

李玫瑾:为什么性格比能力更重要?

军事要闻

伊媒发布小学被炸瞬间 战斧导弹从天而降

无障碍浏览 进入关怀版