还不懂怎么设计超时关单?一文告诉你!

news2024/10/3 23:28:38

背景介绍

​ 提交订单,是交易领域避不开的一个话题。在提交订单设计时,会涉及到各种资源的预占:如商品库存、优惠券数量等等。但是,下单的流程并不能总是保证成功的,如商品库存异常的时候被拦截、优惠券数量不足的时候拦截等等。因此自然会涉及到需要将已被预占的资源回退。

​ 以我现有负责的交易流程为例,一个基本的下单的逻辑大致如下所示:

在这里插入图片描述

​ 正常的下单流程实现较为简单,但是要想处理好异常情况下的数据回滚却是件难事。为此,我大致总结了现有常见的回滚方案。

定时任务

​ 最常见到的订单回滚的实现方案,其实就是采用定时任务扫描的方式。通过单独建立一个任务表,然后启动任务扫描,对扫描到的订单都做一次判断,如果实际上订单没有生成,那么此时进行相应的回滚操作,具体流程如下所示:

在这里插入图片描述

​ 实现定时任务的方式有很多,如通过SpringBoot自带的注解@Scheduled、特定的类库quartz等等。这里我简单写了一段根据Spring自带注解实现的扫描表回退预占的代码:

@Component
@Slf4j
public class RollBackSchedule {

    @Scheduled(cron ="*/6 * * * * ?")
    public void rollBack() {
        //扫描当前的订单task表
        taskService.selectByExample(...);
        //判断当前的订单号orderNo是否已经生成实际订单
        Order order = orderService.selectByOrderNo(...);
        if (order != null){
            //若当前订单已经生成
            return;
        }
        //回滚优惠券
        couponService.rollBack(...);
        //回滚商品库存
        stockService.rollBack(...);
    }
}

优点

​ 采用定时任务实现的方案,实现思路相对简单,实现成本较小。

劣势

​ 定时任务查表,给数据库会带来较大的查询压力,只适合较小的业务数据量。同时,由于被扫描到的具体时间是无法控制的,只能通过控制扫描的时间间隔来尽量精确实现,因此,如果对于实效性较高的系统,定时任务也比较难满足需求。

延迟队列

​ 进一步的,提交订单除了采用定时任务轮训的方式,也可以采用延迟队列的实现方式。简单描述来说,就是首先将订单号生成出来,并放入延迟队列中,消费者则实时监听延迟队列中的消息。如果有消息生成了,那么此时进行消费。大致流程如下所示:

在这里插入图片描述

​ 要实现延迟队列也不是一个难题,这里我简要介绍一下如何采用Spring自带的延迟队列实现:

生产者:

@Slf4j
@Component
public class DelayQueueProducer {

    /**
     * @param orderNo  业务id
     * @param time 消费时间  单位:毫秒
     */
    public void produceTask(String orderNo, Long time){
        DelayQueue<DelayOrder> delayQueue = DelayTaskQueue.getInstance();//创建队列 1
        DelayOrder delayOrder = DelayOrder.builder()
                .orderNo(orderNo)
                .timeout(time)
                .build();
        boolean offer = delayQueue.offer(delayOrder);//任务入队
        if(offer){
            LOGGER.info("=============入队成功,{}",delayQueue);
        }else{
            LOGGER.error("=============入队失败!");
        }
    }
}

消费者:

@Slf4j
@Component
public class RollBackDelayQueueConsumer implements CommandLineRunner {
    
    @Override
    @SneakyThrows
    public void run(String... args) {
        DelayQueue<DelayOrder> delayQueue = DelayTaskQueue.getInstance();//获取同一个put进去任务的队列
        new Thread(() -> {
            while (true) {
                try {
                    // 从延迟队列的头部获取已经过期的消息
                    // 如果暂时没有过期消息或者队列为空,则take()方法会被阻塞,直到有过期的消息为止
                    DelayOrder delayOrder = delayQueue.take();
                    String orderNo = delayOrder.getOrderNo();
                    
                    //判断当前的订单号orderNo是否已经生成实际订单
                    Order order = orderService.selectByOrderNo(orderNo);
                    if (order != null){
                        //当前订单已经生成
                        return;
                    }
                    //回滚优惠券
                    couponService.rollBack(...);
                    //回滚商品库存
                    stockService.rollBack(...);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

优点

内存队列操作,处理效率十分高效

劣势

由于缺少持久化,服务重启会丢失回滚数据;大量请求的情况下容易出现OOM问题;

时间轮算法

​ 时间轮算法也是一种时常被提及到的超时回滚方案。在时间轮算法中,有三个比较重要的参数:ticksPerWheel(一轮的 tick 数),tickDuration(一个 tick 的持续时间)以及 timeUnit(时间单位)

在这里插入图片描述

​ 如果tickPerWheel=60,tickDuration=1,timeUnit=秒,那么其实此时就跟我们日常的时钟是一摸一样了。那么如果我们期望一个任务130s后执行,那么该怎么设置呢?

​ 首先通过130/60=2,我们知道要执行的时间至少需要我们当前的时钟执行两轮,这里我们记作round=2。同时,由于130%60=10,那么此时我们知道这个任务需要被放在第10个位置上。于是,我们就可以在第十个位置上放下一个Round=2的任务,每当指针经过一次10号位置,我们就将该任务的round-1,直到round值等于0的时候,我们就可以执行该任务了。

定时订单任务

class OrderTask implements TimerTask {
    String orderNo;
    public MyTimerTask(String orderNo){
        this.orderNo = orderNo;
    }

    public void run(Timeout timeout) {
        String orderNo = this.orderNo;

        //判断当前的订单号orderNo是否已经生成实际订单
        Order order = orderService.selectByOrderNo(orderNo);
        if (order != null){
            //当前订单已经生成
            return;
        }
        //回滚优惠券
        couponService.rollBack(...);
        //回滚商品库存
        stockService.rollBack(...);
    }
}

主函数部分:

    @SneakyThrows
    public static void main(String[] argv) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        HashedWheelTimer timer = new HashedWheelTimer(1, TimeUnit.MILLISECONDS,8);
        TimerTask timerTask = new MyTimerTask("");

        //将定时任务放入时间轮
        timer.newTimeout(timerTask, 4, TimeUnit.SECONDS);
        Thread.currentThread().join();
    }

优点:

内存操作速度快、实现简单不引入中间件。

劣势:

容易出现OOM、内存数据重启后易丢失。

Redis键过期订阅

​ 除了采用上述的超时回滚方案,我们也可以借助于Redis的键过期订阅的能力实现超时回滚方案。

在这里插入图片描述

​ 实现代码如下:

Key过期配置类

@Configuration
public class KeyExpireConfig {
    
    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        return container;
    }
}

Key过期监听器

@Component
@Slf4j
public class OrderRollBackSubscriber extends KeyExpirationEventMessageListener {

    /**
     * Creates new {@link MessageListener} for {@code __keyevent@*__:expired} messages.
     *
     * @param listenerContainer must not be {@literal null}.
     */
    public OrderRollBackSubscriber(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    //监听的关键
    public void onMessage(@Nullable Message message, byte[] pattern) {
        LOGGER.info("监听到的Key为{}", message);
        String key = new String(message.getBody());
        if (key.startsWith("order")){
            LOGGER.info("执行订单回滚流程");
            ......
        }
    }
}

优点:

​ 实现相对简单、过期时间相对精确、分布式保存服务重启不会丢失。

劣势:

​ 发布订阅采用的是短链接的方式,因此并不能保证能够准确消费到对应的事件;且订阅的消息没有开启持久化;另外一方面,如果出现大数据量时,订阅消费的时间可能会不精确。

消息队列

​ 除了上述采用到的实现方式,超时回滚中,消息队列也是十分重要的一类实现方式。首先,在消息队列的细分中,也包含很多实现:

1、采用rabbitMq通过TTL+死信队列实现;

2、采用Kafka检查放回实现;

3、采用RocketMq延迟消息实现;

​ 方案上各有优劣,方案一方案成熟且实现简单,但是rabbitMq本身吞吐量小,难以处理大批量的业务;kafka支持大吞吐量的处理业务,但是没有现成的延迟方案实现机制,需要自行开发。而方案三支持大吞吐量且也有比较成熟的延迟消息实现机制,但是延迟的时间是按照刻度做处理的,没法做到精确的延迟。

​ 鉴于实际场景和方案的抉择,大部分情况会选择方案三,因此这里我主要围绕方案三展开介绍。整体流程上,消息队列实现的流程同内存的延迟队列实现是基本一致的:

在这里插入图片描述

发送消息:

@Service
@Slf4j
public class RocketMqServiceImpl implements RocketMqService {
   @Resource
   private RocketMQTemplate rocketMQTemplate;

   @Value("${rocketmq.producer.topics[0]}")
   private String topic;

   @PostConstruct
   private void init() {
      rocketMQTemplate.getProducer().getDefaultMQProducerImpl().registerSendMessageHook(new SwimLaneSendMessageHook());
   }

   @Override
   public SendResult sengDelayMessage(String uniqId, String msgInfo, int level, String tag) {
      SendResult sendResult = null;
       /** 创建消息,并指定Topic,Tag和消息体 */
       Message sendMsg = new Message(topic, tag, msgInfo.getBytes());
       sendMsg.setDelayTimeLevel(level);
       /** 发送带规则的延迟消息 */
       sendResult = rocketMQTemplate.getProducer().send(sendMsg, (mqs, msg, arg) -> {
           try {
               String uniqIdStr = String.valueOf(arg);
               String orderNo = StringUtils.substring(uniqIdStr, 1, uniqIdStr.length());
               long id = Long.parseLong(orderNo);
               long index = id % mqs.size();
               return mqs.get(Integer.parseInt(index + ""));
           } catch (Exception e) {
               return mqs.get(1);
           }
       }, uniqId);
      LOGGER.info("发送延迟消息: {}", sendResult);
      return sendResult;
   }
}

消息监听:

@Slf4j
@Component
@RocketMQMessageListener(
   topic = "${rocketmq.consumer.listener.topic}",
   consumerGroup = "${rocketmq.consumer.listener.group}",
   messageModel = MessageModel.CLUSTERING,
   consumeMode = ConsumeMode.ORDERLY
)
public class RocketMsgListener implements RocketMQListener<MessageExt> {

   @Override
   public void onMessage(MessageExt messageExt) {
       if(messageExt.getTags().equals("order")){
           //执行订单回滚代码
           ......
       }
   }
}

优点:

现成方案完备、实现相对简单;

劣势:

预占时间无法自定义,仅有1s、5s等特定的时间间隔;

总结

​ 本文介绍了常见的五种实现超时回滚的方案,分别是:定时任务扫描、延迟队列、时间轮算法、Redis键过期订阅和消息队列。本质上来说,消息队列实现和Redis键过期订阅的方案完备性较好,优先推荐这两种实现方式。但是如果只是简单的单机应用或者是低数据量的情况下,考虑到实现、运维成本的情况下,采用前三种方案也是可行的。没有最好的方案,只有最合适的方案。

参考文献

springBoot之延时队列

订单30分钟未支付自动取消怎么实现?

redis过期key监听与发布订阅功能java

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/422005.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

3月更新 | Visual Studio Code Python

我们很高兴地宣布&#xff0c;2023年3月版 Visual Studio Code Python 和 Jupyter 扩展现已推出&#xff01; 此版本包括以下改进&#xff1a; 后退按钮和取消功能添加到创建环境命令默认情况下&#xff0c;Python 扩展不再附带 isortJupyter 笔记本中内核选择的改进Python P…

Modbus 协议详解

Modbus 协议详解 通信协议是指双方实体完成通信或服务所必须遵循的规则和约定&#xff0c;例如我们为实现人与人之间的交流需要约定统一的语言&#xff0c;统一的文字&#xff0c;规定语速等等。 而对于设备之间&#xff0c;协议定义了数据单元使用的格式&#xff08;例如大端…

四、数组、切片,映射

一、一维数组 //声明一个包含5个元素的整型数组 var array [5]int //具体数值填充数组 array : [5]int{1, 2, 3, 4, 5} //容量由初始化值的数量决定 array : [...]int{1, 2, 3, 4, 5) //只初始化索引为1和2的元素 array : [5]int{1: 10, 2: 20} //修改索引为2的元素的值 array…

Linux文件系统、虚拟内存、进程与线程、锁

文章目录文件系统suLinux 中默认没有 super 命令/proc/etc/var/root/home/bin/dev/lib/sbintmp句柄maxfdPWDpathhomeexportwdfdu虚拟内存jobsLinux下一切皆文件swaponmkswap进程与线程nohup子进程与父进程unix进程间的通信方式线程的同步方式sedtarhistory硬链接ln&#xff08;…

Go分布式爬虫笔记(二十一)

文章目录21 切片和哈希表切片底层结构截取扩容哈希表原理哈希碰撞拉链法开放寻址法&#xff08;Open Addressing&#xff09;读取重建原理删除原理思考题Go 的哈希表为什么不是并发安全的&#xff1f;在实践中&#xff0c;怎么才能够并发安全地操作哈希表&#xff1f;拉链法开放…

软件设计师笔记-----程序设计语言与语言处理程序基础

文章目录七、程序设计语言与语言处理程序基础7.1、编译与解释&#xff08;低频&#xff09;7.2、文法&#xff08;低频&#xff09;7.3、有限自动机与正规式&#xff08;几乎每次都会考到&#xff09;有限自动机正规式7.4、表达式&#xff08;偶尔考到&#xff09;7.5、传值和传…

2023-详解实时数仓建设

一、实时数仓建设背景 1. 实时需求日趋迫切 目前各大公司的产品需求和内部决策对于数据实时性的要求越来越迫切&#xff0c;需要实时数仓的能力来赋能。传统离线数仓的数据时效性是 T1&#xff0c;调度频率以天为单位&#xff0c;无法支撑实时场景的数据需求。即使能将调度频…

网狐大联盟增加账号登陆功能

1. UI设计 2. 发布CSB文件,并添加到前端工程资源目录下 打开已发布csb文件所有目录 复制到工程目录 如果有用到其它目录的资源也要同步复制到工程资源对应目录中: 2.脚本功能编写 增加前端结构: -- 帐号登录 login.CMD_MB_LogonAccounts= {{t = "word", k = &#

企业电子招标采购系统源码—企业战略布局下的采购寻源

​ 智慧寻源 多策略、多场景寻源&#xff0c;多种看板让寻源过程全程可监控&#xff0c;根据不同采购场景&#xff0c;采取不同寻源策略&#xff0c; 实现采购寻源线上化管控&#xff1b;同时支持公域和私域寻源。 询价比价 全程线上询比价&#xff0c;信息公开透明&#xff…

一站式智慧仓储物流方案,免费帮你一屏搞定,领导不重用你都难!

在江苏无锡&#xff0c;菜鸟已经通过柔性自动化技术搭建了亚洲规模最大的无人仓&#xff0c;超过1000台无人车可以快速组合、分拆作业&#xff0c;生产效率可提升一倍多&#xff0c;大大节省了人工成本。智慧仓储物流作为物流的重要一环&#xff0c;也吸引了广泛关注。2022年双…

如何使用 Jetpack Compose 创建翻转卡片效果

如何使用 Jetpack Compose 创建翻转卡片效果 介绍 在电子商务和银行应用程序中输入卡信息是很常见的情况。我认为让用户更轻松地处理这种情况并创建更吸引眼球的 UI 将很有用。大多数应用程序/网站都喜欢它。 执行 在开发阶段&#xff0c;您需要做的是打开一个 Android 项目…

vim命令模式指令一览

提示&#xff1a;本文介绍了linux下vim中的快捷指令。 文章目录注意&#xff1a;本文所有指令都只在命令行模式下有效&#xff01;&#xff01;&#xff01; vim指令图&#xff1a; 指令解析命令解析h光标向左移动j光标向下移动k光标向上移动l光标向下移动yy/nyy复制当前行/赋…

2023最新面试题-Java-1

知其然知其所以然 Java之父&#xff1a;詹姆斯高斯林 (James Gosling)。 什么是Java Java是一门面向对象编程语言&#xff0c;不仅吸收了C语言的各种优点&#xff0c;还摒弃了C里难以理解的多继承、指针等概念。意思Java不支持多继承、指针。Java语言具有功能强大和简单易用…

《花雕学AI》14:免费打开就可用,ChatGPT国内12个镜像站盘点与测试

引言 人工智能聊天机器人是能和人说话的智能系统&#xff0c;它可以帮人做很多事。现在&#xff0c;人工智能聊天机器人很厉害&#xff0c;很多人想试试。ChatGPT是一个很厉害的人工智能聊天机器人&#xff0c;是OpenAI做的。它可以和人一样说话&#xff0c;还可以回答问题、承…

无线耳机哪个音质比较好?四百内音质最好的无线耳机排行

蓝牙耳机常常作为手机的伴生产品而出现在人们的日常生活当中&#xff0c;其使用场景也越来越广泛。而随着蓝牙技术的发展&#xff0c;蓝牙耳机在音质上的表现也越来越好。下面&#xff0c;我来给大家推荐几款四百内音质最好的无线耳机&#xff0c;一起来看看吧。 一、南卡小音舱…

射频功率放大器在液体超声声强的光电测量中的应用

实验名称&#xff1a;液体中超声声强的光电测量 研究方向&#xff1a;光电测量 测试目的&#xff1a; 声强是描述声场的基本物理量口&#xff0c;超声效应直接与声强有关。例如在工程技术领域&#xff0c;液体中的声场分布直接影响流场分布口&#xff0c;声强的大小影响着超声波…

腾讯云GPU云服务器、CVM云服务器、轻量应用服务器配置价格表

这就是腾讯云GPU云服务器、CVM云服务器、轻量应用服务器配置价格表&#xff0c;最近整理的。目前腾讯云服务器分为轻量应用服务器、CVM云服务器和GPU云服务器&#xff0c;首先介绍一下这三种服务器。 1、GPU 云服务器&#xff08;Cloud GPU Service&#xff0c;GPU&#xff09;…

主从模式、哨兵模式、集群模式(cluster)

主从模式、哨兵模式、集群模式&#xff08;cluster&#xff09; redis 实现高可用的方式分为 主从模式、哨兵模式、集群模式&#xff08;cluster&#xff09; 1. 主从模式&#xff08;又称为主从复制&#xff09; 表现为1个主节点&#xff0c;多个从节点&#xff0c;主节点负…

2023年Python选择题及答案解析【35道】

2023年Python练习题及答案解析1、在Python3中&#xff0c;运行结果为&#xff1a;2、在Python3中&#xff0c;字符串的变换结果为&#xff1a;3、在Python3中&#xff0c;下列程序运行结果为&#xff1a;4、在Python3中&#xff0c;下列程序结果为&#xff1a;5、a与b定义如下&…

【C++】基础篇

C基础篇什么是C命名空间命名空间的三种使用方式C的输入和输出缺省参数缺省参数分类函数重载引用引用的使用场景常引用指针和引用的区别auto关键字auto使用细则auto不能推导的场景基于范围的for循环范围for的使用条件指针空值nullptr什么是C 1982年&#xff0c;Bjarne Stroustr…