订单超时未支付的解决方案

  • 定时任务实现
  • 被动取消
  • JDK的延迟队列
    • 代码演示
  • 时间轮算法
    • 代码演示
  • Redis zset 实现延迟任务
    • 代码演示
  • MQ 延时消息
    • 代码演示
  • 写在最后

在前面的文章 第三方支付接口设计中我留了一个问题: 订单超时关闭。这个问题在面试当中也是经常被问到,本文我们就来展开说明一下。

和订单超时关闭类似的场景还有:

  • 淘宝自动确认收货;
  • 微信红包24小时未查收,需要延迟退还;
  • 滴滴预约打车的时候,十分钟没有司机接单,系统会自动取消。

针对上述这些,到了目标时间,系统自动触发代替用户执行的任务,有一个专业的名字:延迟任务。

对于这一类需求我们最先想到的一般就是使用定时任务,通过扫描数据库符合条件的数据,并对其进行更新操作。

延迟任务和定时任务的区别

  1. 定时任务有固定的触发时间,而延迟任务不固定,它依赖于业务事件的触发时间。(比如,取消订单是在生成订单后的半个小时);
  2. 定时任务是周期性的,而延迟任务被触发之后,就结束了,一般是一次性的;
  3. 定时任务一般处理的是多个任务,延迟任务一般是一个任务。

我们下面来看一下定时任务的实现。

定时任务实现

定时任务的实现有这么几种方式:

  • JDK自带Timer实现
  • Quartz框架实现
  • Spring3.0以后自带的task
  • 分布式任务调度:XXL-Job

大概逻辑如下:

假设订单表:t_order(id,end_time,status);

数据扫描:

select id from t_order where end_time>=30 and status=初始状态;

修改:

update t_order set status=结束 where id in (超时订单id);

:如果超时的订单数量很大,就需要分页查询。

这种方式的优点是实现简单,支持分布式/集群环境。

缺点:

  1. 通过轮询不断地扫描数据库,如果数据量很大,并且任务的执行间隔时间较短,对数据库会造成一定的压力;
  2. 间隔时间粒度不好设置;
  3. 存在延迟:如果设置5分钟扫描一次,那么最坏的延迟时间就是5分钟。

被动取消

被动取消和懒加载的思想一致。当用户查询订单的时候,去判断订单是否超时,如果是,走超时的逻辑。

这种方式依赖用户的查询操作。如果用户一直不查询,那么订单就一直不会被取消。

这种方法就是实现简单,不需要增加额外的处理操作。缺点是时效性低,影响用户的体验。

现在也有用定时任务+被动取消的组合方式实现。

上面讲的是定时任务的解决方案,下面我们具体讲一讲延迟任务常见的技术实现。

JDK的延迟队列

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

元素只有在延迟期满时才能从队列中取走。并且队列是有序的,队头放置的元素延迟到期时间最长。

代码演示

定义元素类,作为队列的元素:

public class MyDelayedTask implements Delayed {private String orderId;private long startTime;private long delayMillis;public MyDelayedTask(String orderId, long delayMillis) {this.orderId = orderId;this.startTime = System.currentTimeMillis();this.delayMillis = delayMillis;}/*** 获得延迟时间** @param unit* @return*/@Overridepublic long getDelay(TimeUnit unit) {return unit.convert((startTime + delayMillis) - System.currentTimeMillis(), TimeUnit.MILLISECONDS);}/*** 队列里元素的排序依据** @param o* @return*/@Overridepublic int compareTo(Delayed o) {return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));}public void exec() {System.out.println(orderId + "编号的订单要删除啦!!!");}
}

测试:

public static void main(String[] args) throws InterruptedException {List<String> list = new ArrayList<String>();list.add("00000001");list.add("00000002");list.add("00000003");list.add("00000004");list.add("00000005");long start = System.currentTimeMillis();for (int i = 0; i < list.size(); i++) {//延迟 3sdelayQueue.put(new MyDelayedTask(list.get(i), 3000));delayQueue.take().exec();System.out.println("After " + (System.currentTimeMillis() - start) + " MilliSeconds");}}

结果打印:

00000001编号的订单要删除啦!!!
After 3004 MilliSeconds
00000002编号的订单要删除啦!!!
After 6009 MilliSeconds
00000003编号的订单要删除啦!!!
After 9012 MilliSeconds
00000004编号的订单要删除啦!!!
After 12018 MilliSeconds
00000005编号的订单要删除啦!!!
After 15020 MilliSeconds

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

  • 服务器重启后,数据全部消失,怕宕机
  • 集群扩展相当麻烦
  • 因为是无界队列,如果任务太多的话,那么很容易就出现OOM异常
  • 代码复杂度较高

时间轮算法

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

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

代码演示

依赖:

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

Demo:

public class HashedWheelTimerTest {private static final long start = System.currentTimeMillis();public static void main(String[] args) {// 初始化netty时间轮HashedWheelTimer timer = new HashedWheelTimer(1, // 时间间隔TimeUnit.SECONDS,10); // 时间轮中的槽数TimerTask task1 = new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {System.out.println("已经过了" + costTime() + " 秒,task1 开始执行");}};TimerTask task2 = new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {System.out.println("已经过了" + costTime() + " 秒,task2 开始执行");}};TimerTask task3 = new TimerTask() {@Overridepublic 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);}private static Long costTime() {return (System.currentTimeMillis() - start) / 1000;}
}

Redis zset 实现延迟任务

zset是一个有序集合,ZSet结构中,每个元素(member)都会有一个分值(score),然后所有元素按照分值的大小进行排列。

我们将订单超时时间戳与订单号分别设置为 scoremember。也就是说集合列表中的记录是按执行时间排序,我们只需要取小于当前时间的即可。

代码演示

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;import java.time.LocalDateTime;
import java.util.Set;
import java.util.UUID;@Configuration
public class RedisDelayDemo {@Autowiredprivate RedisTemplate redisTemplate;public void setDelayTasks(long delayTime) {String orderId = UUID.randomUUID().toString();Boolean addResult = redisTemplate.opsForZSet().add("delayQueue", orderId, System.currentTimeMillis() + delayTime);if (addResult) {System.out.println("添加任务成功!" + orderId + ", 当前时间为" + LocalDateTime.now());}}/*** 监听延迟消息*/public void listenDelayLoop() {while (true) {// 获取一个到点的消息Set<String> set = redisTemplate.opsForZSet().rangeByScore("delayQueue", 0, System.currentTimeMillis(), 0, 1);// 如果没有,就等等if (set.isEmpty()) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 继续执行continue;}// 获取具体消息的keyString it = set.iterator().next();// 删除成功if (redisTemplate.opsForZSet().remove("delayQueue", it) > 0) {// 拿到任务System.out.println("消息到期" + it + ",时间为" + LocalDateTime.now());}}}
}

测试:

@RequestMapping("/delayTest")
public void delayTest() {delayDemo.setDelayTasks(5000L);delayDemo.listenDelayLoop();
}

结果打印:

添加任务成功!e99961a0-fc1d-43d4-a83e-8db5fb6b3273, 当前时间为2021-10-24T12:06:59.037363700
消息到期e99961a0-fc1d-43d4-a83e-8db5fb6b3273,时间为2021-10-24T12:07:04.097486

优点:

  1. 集群扩展方便
  2. 时间准确度高
  3. 不用担心宕机问题

缺点:需要额外进行redis维护。在高并发条件下,多消费者可能会取到同一个订单号。这种情况可以增加一个分布式锁来处理,但是,性能下降严重。

MQ 延时消息

我们可以通过MQ延时消息实现,以RocketMQ举例。

通常的消息在投递后会立马被消费者所消费,而延时消息在投递时,需要设置指定的延时级别(不同延迟级别对应不同延迟时间),即等到特定的时间间隔后消息才会被消费者消费,这样就将数据库层面的压力转移到了MQ中,也不需要手写定时器,降低了业务复杂度,同时MQ自带削峰功能,能够很好的应对业务高峰。

代码演示

依赖:

<dependency><groupId>org.apache.rocketmq</groupId><artifactId>rocketmq-client</artifactId><version>5.0.0-PREVIEW</version>
</dependency>

生产者demo:

@Component
public class ProducerSchedule {private DefaultMQProducer producer;@Value("${rocketmq.producer.producer-group}")private String producerGroup;@Value("${rocketmq.namesrv-addr}")private String nameSrvAddr;public ProducerSchedule() {}/*** 生产者构造** @PostConstruct该注解被用来修饰一个非静态的void()方法 Bean初始化的执行顺序:* Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法)*/@PostConstructpublic void defaultMQProducer() {if (Objects.isNull(this.producer)) {this.producer = new DefaultMQProducer(this.producerGroup);this.producer.setNamesrvAddr(this.nameSrvAddr);}try {this.producer.start();System.out.println("Producer start");} catch (MQClientException e) {e.printStackTrace();}}/*** 消息发布** @param topic* @param messageText* @return*/public String send(String topic, String messageText) {Message message = new Message(topic, messageText.getBytes());/*** 延迟消息级别设置* messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h*/message.setDelayTimeLevel(4);SendResult result = null;try {result = this.producer.send(message);System.out.println("返回信息:" + JSON.toJSONString(result));} catch (Exception e) {e.printStackTrace();}return result.getMsgId();}
}

消费者demo:

@Component
public class ConsumerSchedule implements CommandLineRunner {@Value("${rocketmq.consumer.consumer-group}")private String consumerGroup;@Value("${rocketmq.namesrv-addr}")private String nameSrvAddr;@Value("${rocketmq.topic}")private String rocketmqTopic;public void messageListener() throws MQClientException {DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(this.consumerGroup);consumer.setNamesrvAddr(this.nameSrvAddr);/*** 订阅主题*/consumer.subscribe(rocketmqTopic, "*");/*** 设置消费消息数*/consumer.setConsumeMessageBatchMaxSize(1);/*** 注册消息监听*/consumer.registerMessageListener((MessageListenerConcurrently) (messages, context) -> {for (Message message : messages) {System.out.println("监听到消息:" + new String(message.getBody()));}return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;});consumer.start();}@Overridepublic void run(String... args) throws Exception {this.messageListener();}
}

设置消息延时级别的方法是setDelayTimeLevel(),目前RocketMQ不支持任意时间间隔的延时消息,只支持特定级别的延时消息。

写在最后

今天是[2021-10-24],祝大家 程序员节快乐,早日实现自己的小目标!!!

如果你还想看更多优质原创文章,欢迎关注我的公众号「ShawnBlog」。

订单超时未支付的解决方案相关推荐

  1. 订单超时未支付自动关闭的几种实现方案

    做电商,就会遇到订单超时问题,而且还经常被拿来面试提问! 今天,周末放假,抽时间给大家总结了几种订单超时未支付自动关闭的实现方案.同时,我手机还有几套电商类从零架构到实现的视频教程,如有需要,可以加我 ...

  2. 处理超时订单(超时未付款)的解决方案

    文章目录 1. 超时未支付订单处理 1.1 需求分析 1.2 实现思路 1.3 rabbitmq延迟消息 1.3.1 消息的TTL(Time To Live) 1.3.2 死信交换器 Dead Let ...

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

    " 在开发中,往往会遇到一些关于延时任务的需求.例如:生成订单 30 分钟未支付,则自动取消:生成订单 60 秒后,给用户发短信. 对上述的任务,我们给一个专业的名字来形容,那就是延时任务. ...

  4. 订单超时未支付自动取消5种实现方案

    大家好,我是宝哥! 前言 在开发中,往往会遇到一些关于延时任务的需求.比如最近大家都在忙抢回家的火车票,当你下了一个订单没有支付时,会有一个倒计时,提示你半小时之内支付,否则会自动取消.这样的场景是如 ...

  5. rabbitMQ实现订单超时未支付自动取消订单

    前期准备 下面展示一些 内联代码片. 1.配置文件,导入jar包 server:port: 8983 spring:application:name: API-RABBITMQdatasource:t ...

  6. 订单超时未支付自动取消8种实现方案

    定时轮询 数据库定时轮询方式,实现思路比较简单.启动一个定时任务,每隔一定时间扫描订单表,查询到超时订单就取消. 优点:实现简单. 缺点:轮询时间间隔不好确定,占用服务器资源,影响数据库性能. 惰性取 ...

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

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

  8. 1.超时未支付订单处理

    1.超时未支付订单处理 1.1 需求分析 超过限定时间并未支付的订单,我们需要进行超时订单的处理:先调用微信支付api,查询该订单的支付状态.如果未支付调用关闭订单的api,并修改订单状态为已关闭,并 ...

  9. TP5.1 实现超时未支付订单自动关闭

    对于这个需求,我以前写过Laravel版本的.今天想在TP5.1中实现这个功能,但是网上基本没什么教程可供参考,所以写篇文章仅供大家学习. 一.前台 1.先来加载订单确认页面 当下单成功后,通过 js ...

最新文章

  1. 关于动态规划,你想知道的都在这里了!
  2. 云从科技3D人体重建技术刷新3项纪录!仅凭照片即可生成精细模型
  3. 深入理解ReactRedux
  4. objective-c对NSArray的学习
  5. 关于在Android中一个XML文件包含另外一个XML的方法
  6. 迭代器、生成器、装饰器
  7. adams2015安装教程
  8. “约见”面试官系列之常见面试题第十七篇之实现深拷贝(建议收藏)
  9. 摘:多线程和异步的区别
  10. ul c语言,IMX6UL裸机实现C语言蜂鸣器实验
  11. ebs开发入门 oracle 知乎_微信小程序云开发入门第一篇---开发准备事项
  12. node.js中net网络模块TCP服务端与客户端的使用
  13. Android模块化之MicroModule(微信Pins工程) 1
  14. 运算符重载的非成员函数形式
  15. 17.3.10--关于C元的变量类型所占字节问题和类型转化
  16. AI CC2017安装后,安装目录里找不到amtlib.dll文件的问题
  17. 【Python - OpenCV】数字图像项目实战(四) - 位姿估计
  18. 苹果开发者账号注册-您在注册时提供的地址无效或者不完整
  19. 找出01二维矩阵中只包含 1 的最大正方形,并输出其面积_java
  20. 技术实操|Apache Spark 内存管理详解(上篇)

热门文章

  1. [Codewar练习-java]Snail Sort(蜗牛型排序)
  2. linux建立数据库及构建表
  3. 2、GO语言多进程编程
  4. 方法的重写和重载,接口和抽象类区别
  5. mysql安装流程以及各类问题解决
  6. js判断数据类型常用的四种方法
  7. 中国计算机艺术的发展趋势,浅谈数字媒体艺术的发展趋势
  8. 技术分享 | 无人机集群——分布式控制算法
  9. 使用 memory_limit 限制 PHP 进程的内存使用
  10. 微信小程序中使用ECharts--折线图、柱状图、饼图等