支付超时取消订单实现方案 - 定时任务、延迟队列、消息队列等

这篇具有很好参考价值的文章主要介绍了支付超时取消订单实现方案 - 定时任务、延迟队列、消息队列等。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1.业务场景

在实际业务场景中,我们经常会碰到类似一下场景:

  • 淘宝等购物平台在订单支付时,如果30分钟内未支付自动取消。
  • 腾讯会议预约会议后,在会议开始前15分钟提醒。
  • 未使用的优惠券有效期结束后,自动将优惠券状态更新为已过期。
  • 等等。。。

像这种支付超时取消的场景需求,其实有很多种实现方法,比如定时任务轮询、Java中的延时队列、时间轮算法、Redis过期监听等,如下图所示。

支付超时取消订单实现方案 - 定时任务、延迟队列、消息队列等,后端技术实现方案,消息队列,时间轮算法,定时任务,Redis过期监听

2.定时任务(Quartz)

Java中常见的定时任务框架包括 Quartz、Spring Task、Elastic-Job、XXL-Job等。下面将以 Quartz 为例实现业务场景(有关Elastic-Job 的使用可见 Elastic Job 开发使用篇)。

2.1.依赖导入

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
  	<version>2.3.2</version>
</dependency>

2.2.任务类

@Slf4j
public class PaymentJob implements Job {

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        log.info("查询数据库获取超时支付订单,并取消该订单");
    }

}

2.3.任务调度类

public class RoundRobin {

    private static Scheduler defaultScheduler;

    public void timedTask() {
        // 创建任务明细
        JobDetail jobDetail = JobBuilder.newJob(PaymentJob.class)
                .withIdentity("支付超时取消订单任务", "payment_timeout_group")
                .build();
        // 创建触发器
        Trigger trigger = TriggerBuilder.newTrigger()
                .withDescription("这是支付超时取消订单任务触发器")
                .startNow()
                // 设置任务执行调度周期:cron表达式,每3秒执行一次
                .withSchedule(CronScheduleBuilder.cronSchedule("0/3 * * * * ?"))
                .build();
        // 创建scheduler调度器
        try {
            if (defaultScheduler == null) {
                synchronized (this) {
                    if (defaultScheduler == null) {
                        defaultScheduler = StdSchedulerFactory.getDefaultScheduler();
                    }
                }
            }
            //  执行任务
            defaultScheduler.scheduleJob(jobDetail, trigger);
            defaultScheduler.start();
        } catch (SchedulerException e) {
            throw new RuntimeException(e);
        }
    }

}

2.4.小结

定时任务轮询的方式简单易行,但是这种方式也存在着显著的局限性:

1.在支付订单数量庞大的情况下,每次获取超时订单会走全表扫描,给数据库带来很大的IO负担和CPU占用,特别是这种需要小时间间隔任务轮询的全表扫描。其实这种也有不走全表扫描的方法,牺牲空间,就是对订单创建时间建立索引,设过期时间为 当前时间 - 30分钟(假设是超时时间),走索引查询过期时间之前的所有订单,最后执行取消订单的操作。

2.精度问题。如果将定时任务的时间间隔设置的比较长,会导致超时订单取消延迟较长,影响业务流程。如果间隔时间过于短,在大量订单的情况下,可能会出现大量重复订单,需要考虑并发问题和事务冲突。

3.延迟队列(DelayQueue)

DelayQueue是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走。当生产者线程调用插入元素的方法加入元素时,会触发Delayed接口中的compareTo方法进行排序,也就是说队列中元素的顺序是按到期时间排序的,而非它们进入队列的顺序。排在队列头部的元素是最早到期的,越往后到期时间越晚。

3.1.任务类

@Slf4j
public class OrderDelay implements Delayed {

    // 订单id
    private String orderId;

    // 超时的最后时刻(单位毫秒)
    private long timeout;

    public OrderDelay(String orderId, long timeout) {
        this.orderId = orderId;
        this.timeout = timeout+System.currentTimeMillis();
    }


    // 返回距离超时还剩多少毫秒
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(timeout - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    // 和其他的订单比较时间
    @Override
    public int compareTo(Delayed o) {
        if (o == this) {
            return 0;
        } else {
            OrderDelay t = (OrderDelay) o;
            long l = this.timeout - t.timeout;
            return l == 0 ? 0 : (l > 0 ? 1 : -1);
        }
    }

    // 超时取消处理
    public void timeoutCancel(){
        log.info("订单{}超时,处理完毕",orderId);
    }

}

3.2.测试案例

public class CancelTimeoutOrder {

    public static void main(String[] args) {
        // 先创建3个订单
        OrderDelay o1 = new OrderDelay("1", 2 * 1000);
        OrderDelay o2 = new OrderDelay("2", 4 * 1000);
        OrderDelay o3 = new OrderDelay("3", 6 * 1000);
        // 创建延迟队列
        DelayQueue<Delayed> delayQueue = new DelayQueue<>();
        delayQueue.put(o1);
        delayQueue.put(o2);
        delayQueue.put(o3);
        // 开始判断订单
        while (true){
            try {
                OrderDelay take = (OrderDelay) delayQueue.take();
                take.timeoutCancel();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

}

3.3.日志输出

19:20:36.591 [main] INFO com.payment.demo.delay.OrderDelay - 订单1超时,处理完毕
19:20:38.588 [main] INFO com.payment.demo.delay.OrderDelay - 订单2超时,处理完毕
19:20:40.588 [main] INFO com.payment.demo.delay.OrderDelay - 订单3超时,处理完毕

3.4.小结

这种方式弥补了精度问题,并且任务处理更加高效,也不需要考虑多线程并发性的问题。但是所有订单都需要保留在内存,在大量订单的情况下会有很大的内存消耗,如果此时系统重启或者崩溃,那么剩余未处理的订单将会丢失。

4.时间轮算法

时间轮算法(Time Wheel Algorithm)是一种用于处理定时任务调度的算法,它使用循环数组指针来实现,在每个时刻都有一个指针指向当前时间槽,每个时间槽中保存了需要执行的任务列表。时间轮算法的核心是轮询线程不再负责遍历所有任务,而是仅仅遍历时间刻度。

时间轮算法主要原理如下:

  1. 时间轮的构造:时间轮由多个槽(slot)组成,每个槽表示一个时间间隔。整个时间轮可以看作是一个环状结构,每个槽都有一个索引来标识。

  2. 时间轮的转动:时间轮按照固定的速度不断地转动,每次转动一个槽的间隔(例如,每秒转动一次)。

  3. 任务插入:当需要添加一个延迟任务时,根据任务的延迟时间,计算应该插入到哪个槽中。任务会被插入到离当前时间一定间隔的槽中。

  4. 任务触发:时间轮的每次转动都会检查当前位置的槽是否有任务,如果有,就执行任务。

  5. 时间轮的级联:如果有多个时间轮,可以将多个时间轮级联,即把一个时间轮的一个槽作为下一个时间轮的一个槽。这样可以扩展时间轮的范围和精度。

  6. 任务的删除:当延迟任务被取消或者执行完成时,需要从时间轮中删除对应的任务。

时间轮算法在实际应用中有很多用途,比如网络延迟调度、定时任务调度、消息队列等。通过合理地调整时间轮的大小和刻度,可以实现高效的任务调度和处理。

4.1.依赖导入

<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-common</artifactId>
  	<version>4.1.94.Final</version>
</dependency>

4.2.任务类

@Slf4j
public class OrderTask implements TimerTask {

    // 订单id
    private String orderId;

    public OrderTask(String orderId) {
        this.orderId = orderId;
    }

    // 任务执行方法
    @Override
    public void run(Timeout timeout) throws Exception {
        log.info("订单{}超时,处理完毕",orderId);
    }

}

4.3.测试案例

public class TimeWheelUtil {

    public static void main(String[] args) {
        // 创建任务
        OrderTask o1 = new OrderTask("1");
        OrderTask o2 = new OrderTask("2");
        OrderTask o3 = new OrderTask("3");
        // 时间轮算法实现类
        HashedWheelTimer hashedWheelTimer = new HashedWheelTimer();
        // 添加任务
        hashedWheelTimer.newTimeout(o1,2, TimeUnit.SECONDS);
        hashedWheelTimer.newTimeout(o2,4, TimeUnit.SECONDS);
        hashedWheelTimer.newTimeout(o3,6, TimeUnit.SECONDS);
    }

}

4.4.日志输出

20:04:17.611 [pool-1-thread-1] INFO com.payment.demo.wheel.OrderTask - 订单1超时,处理完毕
20:04:19.610 [pool-1-thread-1] INFO com.payment.demo.wheel.OrderTask - 订单2超时,处理完毕
20:04:21.612 [pool-1-thread-1] INFO com.payment.demo.wheel.OrderTask - 订单3超时,处理完毕

4.5.小结

时间轮算法其实和延迟队列比较相似。与延迟队列相比,其性能更优越,任务触发时间延迟时间更低,代码复杂度更简单。同样,由于信息存储于内存中,所以容易因为系统重启或宕机而丢失订单信息。

5.Redis

我们都知道 Redis 中的 key 可以设置过期时间,显而易见,通过设置过期时间然后监听这个 key 是否过期就能判断支付订单是否超时了。而Redis本身就具备key过期监听功能,即利用 Redis 的Keyspace Notifications功能,当一个 key 过期时,Redis 会向已订阅了相关 channel 的客户端发送一个通知。

5.1.修改配置

首先我们需要打开 redis.conf 文件,开启Keyspace Notifications功能,即修改如下配置。

notify-keyspace-events Ex

如图所示。

支付超时取消订单实现方案 - 定时任务、延迟队列、消息队列等,后端技术实现方案,消息队列,时间轮算法,定时任务,Redis过期监听

随后启动 redis 服务端。

5.2.导入依赖

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.3.0</version>
</dependency>

5.3.测试案例

@Slf4j
public class RedisKeyNotify {

    static JedisPool jedisPool = null;

    public static void main(String args[]) throws InterruptedException {
        new Thread(() -> {
            // 配置redis连接
            jedisPool = new JedisPool("localhost", 6379);
            // 订阅redis的key过期通知
            jedisPool.getResource().subscribe(new RedisSub(),"__keyevent@0__:expired");
        }).start();
        // 等待jedis初始化完
        TimeUnit.SECONDS.sleep(1);
        // 模拟一些数据
        jedisPool.getResource().setex("1",3,"1");
        jedisPool.getResource().setex("2",6,"2");
    }

    static class RedisSub extends JedisPubSub {
        @Override
        public void onMessage(String channel, String message) {
            log.info("订单{}超时,处理完毕",message);
        }
    }

}

5.4.日志输出

23:50:57.500 [Thread-0] INFO com.payment.demo.redis.RedisKeyNotify - 订单1超时,处理完毕
23:51:00.382 [Thread-0] INFO com.payment.demo.redis.RedisKeyNotify - 订单2超时,处理完毕

5.5.小结

Redis的键过期事件处理机制天然支持高并发场景,只要Redis集群足够强大,可以轻松处理大量订单的过期处理。但是这种方式有一个很严重的弊端,在官方网站中有如下提醒:

Note: Redis Pub/Sub is fire and forget 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)模式的,也就是说,如果您的Pub/Sub客户端断开连接,稍后再重新连接,则客户端断开时传递的所有事件都将丢失。因此无法实现事件的可靠通知。

6.消息队列(RocketMQ)

延迟队列可以直接处理延迟消息,即消息在指定的延迟时间过后才被投递给消费者。在支付超时取消订单的场景中,订单创建时将订单信息封装成消息,并设置消息的延迟时间,当订单超时时,消息自动被投递到处理超时订单的队列,消费者接收到消息后执行取消操作。

以 RocketMQ 为例,在 RocketMQ 中没有延迟队列这一概念,但是我们可以通过延迟消息(Delayed Message)实现这一功能。有关RocketMQ的安装部署请移步 《RocketMQ安装部署+简单实战开发》

6.1.延迟级别

RocketMQ 一共支持18个等级的延时投递。

投递等级(delay level) 延迟时间 投递等级(delay level) 延迟时间
1 1s 10 6min
2 5s 11 7min
3 10s 12 8min
4 30s 13 9min
5 1min 14 10min
6 2min 15 20min
7 3min 16 30min
8 4min 17 1h
9 5min 18 2h

6.2.生产者代码

延时消息的实现逻辑需要先经过定时存储等待触发,延时时间到达后才会被投递给消费者。因此,如果将大量延时消息的定时时间设置为同一时刻,则到达该时刻后会有大量消息同时需要被处理,会造成系统压力过大,导致消息分发延迟,影响定时精度。文章来源地址https://www.toymoban.com/news/detail-855651.html

@Component
@Slf4j
public class DelayMsgSend {

    /**
     * 导入RocketMQ模版工具
     */
    @Resource
    private RocketMQTemplate rocketMQTemplate;

    /**
     * 发送延迟消息
     *
     * @param topic      主题
     * @param msg        消息内容 (本次案例为支付订单id)
     * @param timeout    超时时间(单位:毫秒)
     * @param delayLevel 延迟级别
     */
    public void sendDelayMsg(String topic, String msg, int timeout, int delayLevel) {
        // 创建消息载体
        Message<String> build = MessageBuilder.withPayload(msg).build();
        // 同步发送(也可以选择异步发送)
        SendResult sendResult = rocketMQTemplate.syncSend(topic, build, timeout, delayLevel);
        log.info("延迟消息发送成功。发送结果:{}",sendResult);
    }

}

6.3.消费者代码

@Slf4j
@Component
@RocketMQMessageListener(
        topic = "delay_topic",
        consumerGroup = "order_consumer_group",
        selectorType = SelectorType.TAG,
        messageModel = MessageModel.CLUSTERING
)
public class DelayMsgConsumer implements RocketMQListener<String> {
    
    @Override
    public void onMessage(String message) {
        log.info("接收到订单id[{}]。判断是否超时,并执行相关逻辑",message);
    }
    
}

6.4.小结

  • 优点
    • 订单创建、消息发送、支付取消等业务功能都是独立的,有利于系统的模块化和拓展。
    • RocketMQ采用了多种机制保证消息的可靠性传输,如同步刷盘、主从复制等。这意味着一旦消息发送成功,将会被可靠地传输到消息队列中,不易丢失。
    • RocketMQ具备高吞吐量的特点,能够处理大量的消息,并且能动态随订单量调整消费速度。
  • 缺点
    • 由于引入了消息中间件,所以会涉及到消息中间件配置和管理,增加了系统的复杂性。
    • 高度依赖消息中间件的可用性和稳定性。
    • 仍有小概率会丢失信息,这个也是不可避免的,任何方式都没有绝对保证。

到了这里,关于支付超时取消订单实现方案 - 定时任务、延迟队列、消息队列等的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • 生成订单30分钟未支付,则自动取消,该怎么实现?

    生成订单30分钟未支付,则自动取消,该怎么实现?

    今天给大家上一盘硬菜,并且是支付中非常重要的一个技术解决方案,有这块业务的同学注意自己试一把了哈! 在开发中,往往会遇到一些关于延时任务的需求。例如 生成订单30分钟未支付,则自动取消 生成订单60秒后,给用户发短信 对上述的任务,我们给一个专业的名字来

    2024年02月09日
    浏览(10)
  • C# Task 实现任务超时取消、超时取消然后重试 超过重试最大次数就结束。
  • Spring Task 实现定时任务 以及 WebSocket 实现 订单提醒 (学习笔记)

    1.maven坐标 spring-context 2.启动类添加注解 @EnableScheduling 开启任务调度 3.自定义定时任务类 去设置网站设置要 进行得定时任务 cron表达式在线生成器:https://cron.qqe2.com/ 1.导入maven坐标 2.导入websocket组件 3.设置配置类 4.导入代码 前端测试代码

    2024年02月20日
    浏览(9)
  • 复习之系统定时任务及延迟任务

    复习之系统定时任务及延迟任务

    at  +时间 :具体时间设定延迟任务 设定成功后“ ctrl + d \\\"发起任务,\\\" ctrl + c \\\" 取消。 at  -l  :查看延迟任务 at  -c  1 :查看序号为1 的延迟任务的内容 at  -r  1 :取消序号为1 的延迟任务 at  now+1min : 设定1分钟后的延迟任务 ------------------------------------------------------实验-

    2024年02月16日
    浏览(5)
  • java定时任务如何取消

    java定时任务如何取消,并比如,我之前想每周二晚上6点自动生成一条devops流水线,现在我想停掉 答案: 在Java中,可以使用 ScheduledExecutorService 类来创建定时任务。要取消定时任务,可以调用 ScheduledFuture 对象的 cancel() 方法。 以下是一个示例代码,演示如何创建一个每周二

    2024年02月11日
    浏览(5)
  • Android常用的延迟执行任务及轮询定时任务的几种方式

    Executor 的 execute 方法:向线程池中提交任务(异步执行) Executor 接口是 Java 并发编程中的一个接口,它定义了一种执行任务的通用机制。Executor 接口有一个重要的方法 execute,它的作用是提交一个任务(Runnable 或 Callable)给 Executor 进行执行。 execute 方法的作用如下: 提交任务:

    2024年04月25日
    浏览(6)
  • 订单自动取消的11种实现方式(下)

    订单自动取消的11种实现方式(下)

    在Redis中,有个发布订阅的机制 生产者在消息发送时需要到指定发送到哪个channel上,消费者订阅这个channel就能获取到消息。图中channel理解成MQ中的topic。 并且在Redis中,有很多默认的channel,只不过向这些channel发送消息的生产者不是我们写的代码,而是Redis本身。这里面就有

    2024年02月13日
    浏览(8)
  • 支付宝定时任务怎么做?三层分发任务处理框架介绍

    支付宝定时任务怎么做?三层分发任务处理框架介绍

      一、背景介绍 技术同学对定时任务肯定不陌生。定时任务一般用来定时批量进行业务处理。支付宝卡包券到期提醒、删除过期失效券,五福大促批量给用户发放添福红包等场景,都是通过定时任务触发来完成的。 作者有幸参与了2023兔年五福大促的开发,主导完成了福气乐

    2023年04月12日
    浏览(6)
  • Spring Boot整合Redis实现订单超时处理

    Spring Boot整合Redis实现订单超时处理

    🎉欢迎来到架构设计专栏~Spring Boot整合Redis实现订单超时处理 ☆* o(≧▽≦)o *☆嗨~我是IT·陈寒🍹 ✨博客主页:IT·陈寒的博客 🎈该系列文章专栏:架构设计 📜其他专栏:Java学习路线 Java面试技巧 Java实战项目 AIGC人工智能 数据结构学习 🍹文章作者技术和水平有限,如果文

    2024年02月03日
    浏览(12)
  • 苍穹外卖项目解读(四) 微信小程序支付、定时任务、WebSocket

    苍穹外卖项目解读(四) 微信小程序支付、定时任务、WebSocket

    HM新出springboot入门项目《苍穹外卖》,笔者打算写一个系列学习笔记,“苍穹外卖项目解读”,内容主要从HM课程,自己实践,以及踩坑填坑出发,以技术,经验为主,记录学习,也希望能给在学想学的小伙伴一个参考。 注:本文章是直接拿到项目的最终代码,然后从代码出发

    2024年02月07日
    浏览(7)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包