订单到期关闭如何实现

news2025/1/12 10:54:18

在电商、支付等系统中,一般都是先创建订单(支付单),再给用户一定的时间进行支付,如果没有按时支付的话,就需要把之前的订单(支付单)取消掉。这种类似的场景有很多,还有比如到期自动收货、超时自动退款、下单后自动发送短信等等都是类似的业务问题。


订单的到期关闭的实现有很多种方式,分别有:

  1. 被动关闭
  2. 定时任务
  3. DelayQueue
  4. 时间轮
  5. kafka
  6. RocketMQ延迟消息
  7. RabbitMQ死信队列
  8. RabbitMQ插件
  9. Redis过期监听
  10. Redis的ZSet
  11. Redisson

一、被动关闭

在解决这类问题的时候,有一种比较简单的方式,那就是通过业务上的被动方式来进行关单操作。

简单点说,就是订单创建好了之后。我们系统上不做主动关单,什么时候用户来问这个订单了,再去判断时间是不是超过了过期时间,如果过了时间那就进行关单操作,然后再提示用户。

这种做法是最简单的,基本不需要开发定时关闭的功能,但是他的缺点也很明显,那就是如果用户一直不来查看这个订单,那么就会有很多脏数据沉余在数据库中一直无法被关单。


还有一个缺点,那就是需要在用户的查询过程中进行写的操作,一般写操作都会比读操作耗时更长,写是指订单的状态修改,这里虽然可以通过异步去执行,不过一旦关单失败了,就会导致系统处理起来比较复杂。


所以,这种方案只适合于自己学习的时候用,任何商业网站中都不建议使用这种方案来实现订单关闭的功能。

二、定时任务

定时任务关闭订单,这是很容易想到的一种方案。


具体实现细节就是我们通过一些调度平台来实现定时执行任务,任务就是去扫描所有到期的订单,然后执行关单动作。
 

这个方案的优点也是比较简单,实现起来很容易,基于Timer、ScheduledThreadPoolExecutor、或者像x-job这类调度框架都能实现,但是有以下几个问题:


1、时间不精准。一般定时任务基于固定的频率、按照时间定时执行的,那么就可能会发生很多订单已经到了超时时间,但是定时任务的调度时间还没到,那么就会导致这些订单的实际关闭时间要比应该关闭的时间晚一些。


2、无法处理大订单量。定时任务的方式是会把本来比较分散的关闭时间集中到任务调度的那一段时间,如果订单量比较大的话,那么就河能导致任务执行时间很长,整个任务的时间越长,订单被扫描到时间可能就很晚,那么就会导致关闭时间更晚。


3、对数据库造成压力。定时任务集中扫表,这会使得数据库在短时间内被大量占用和消耗,如果没有做好隔离,并且业务量比较大的话,就可能会影响到线上的正常业务。


4、分库分表问题。订单系统,一旦订单量大就可能会考虑分库分表,在分库分表中进行全表扫描,这是一个极不推荐的方案。


所以,定时任务的方案,适合于对时间精确度要求不高、并目业务量不是很大的场景中。如果对时间精度要求比较高,并目业务量很大的话,这种方案不适用。

三、JDK自带的DelayQueue

有这样一种方案,他不需要借助任何外部的资源,直接基于应用自身就能实现,那就是基于DK自带的DelayQueue:来实现。

DelayQueue是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能到期了才能从队列中取走。

基于延迟队列,是可以实现订单的延迟关闭的,首先,在用户创建订单的时候,把订单加入到DelayQueue中,然后,还需要一个常驻任务不断的从队列中取出那些到了超时时间的订单,然后再把他们进行关单,之后再从队列中删除掉。

这个方案需要有一个线程,不断地从队列中取出需要关单的订单。一般在这个线程中需要加一个while(true)循环,这样才能确报任务不断的执行并且能够及时的取出超时订单。

使用DelayQueue实现超时关单的方案,实现起来简单,不需要依赖第三方的框架和类库,JDK原生就支持了。

然而此方案也存在一个缺点,首先,基于DelayQueue的话,需要把订单放进去,那如果订单量太大的话,可能会导致OOM的问题;另外DelayQueue是基于JVM内存的,一旦机器重启了,里面的数据就都没有了。虽然我们可以配合数据库的持久化一起使用。而且现在很多应用都是集群部署的,那么集群中多实例上的多个DelayQueue如何配合是一个很大的问题。

所以,基于JDK的DelayQueue方案只适合单机场景、并且数据量不大的场景中使用,如果涉及到分布式场景,那还是不建议使用。

四、Netty的时间轮

还有一种方式,和上面我们提到的JDK自带的DelayQueue类似的方式,那就是基于时间轮实现。

为什么要有时间轮呢?主要是因为DelayQueue插入和删除操作的平均时间复杂度为O(nlog(n)),虽然已经挺好的了,但是时间轮的方案可以将插入和删除操作的时间复杂度都降为O(1)。

时间轮可以理解为一种环形结构,像钟表一样被分为多个slot。每个slot代表一个时间段,每个slot中可以存放多个任务,使用的是链表结构保存该时间段到期的所有任务。时间轮通过一个时针随着时间一个个slot转动,并执行slot中的所有到期任务。

基于Netty的HashedWheelTimer可以帮助我们快速的实现一个时间轮,这种方式和DelayQueue类似,缺点都是基于内存、集群扩展麻烦、内存有限制等等。

但是他相比DelayQueue效率会更高一些,任务触发的延迟更低。代码实现上面也更加精简。

所以,基于Netty的时间轮方案比基于JDK的DelayQueue效率更高,实现起来更简单,但是同样的,只适合单机场景、并且数据量不大的场景中使用,如果涉及到分布式场景,那还是不建议使用。

五、Kafka的时间轮

既然基于Netty的时间轮存在一些问题,那么有没有其他的时间轮的实现呢?

还真有的,那就是Kafka的时间轮,Kafka内部有很多延时性的操作,如延时生产,延时拉取,延时数据删除等,这些延时功能由内部的延时操作管理器来做专门的处理,其底层是采用时间轮实现的。

而且,为了解决有一些时间跨度大的延时任务,Kafka还引入了层级时间轮,能更好控制时间粒度,可以应对更加复杂的定时处理场景:

Kafka中的时间轮是由TimingWheel类实现的,位于kafk.utils.timer包中。基于Kafka的时间轮同样可以得到O(1)时间复杂度,性能上还是不错的。

基于Kafka的时间轮的实现方式,在实现方式上有点复杂,需要依赖kafka,但是他的稳定性和性能都要更高一些,而且适合用于分布式场景中

六、RocketMQ延迟消息

相比于Kafka来说,RocketMQ中有一个强大的功能,那就是支持延迟消息。

延迟消息,当消息写入到Broker后,不会立刻被消费者消费,需要等待指定的时长后才可被消费处理的消息,称为延时消息。

有了延迟消息,我们就可以在订单创建好之后,发送一个延迟消息,比如20分钟取消订单,那就发一个延迟20分钟的延迟消息,然后在20分钟之后,消息就会被消费者消费,消费者在接收到消息之后,去关单就行了。

但是,RocketMQ的延迟消息并不是支持任意时长的延迟的,它只支持:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h 这几个时长。(商业版支持任意时长)

可以看到,有了RocketMQ延迟消息之后,我们处理上就简单很多,只需要发消息,和接受消息就行了,系统之间完全的解耦了。但是因为延迟消息的时长收到了限制,所以并不是很灵活。

如果我们的业务上,关单时长刚好和RocketMQ延迟消息支持的时长匹配的话,那么是可以基于RocketMQ延迟消息来实现的。否则,这种方式并不是最佳的,但是在RocketMQ5.0新增了基于时间轮实现的定时消息,可以解决这个问题。

七、RabbitMQ死信队列

延迟消息不仅在RocketMQ中支持,其实在RabbitMQ中也是可以实现的,只不过其底层是基于死信队列实现的。

当RabbitMQ中的一条正常的消息,因为过了存活时间(TTL过期)、队列长度超限、被消费者拒绝等原因无法被消费时,就会变成Dead Message, 即死信。

当一个消息变成死信之后,他就能被重新发送到死信队列中(其实是交换机-exchange)。

那么基于这样的机制,就可以实现延迟消息了。那就是我们给一个消息设定ttl,但是并不消费这个消息,等他过期,过期后就会进入到死信队列,然后我们再监听死信队列的消息消费就行了。

而且RabbitMQ中的这个TTL是可以设置任意时长的,这就解决了RocketMQ的不灵活的问题。


但是,死信队列的实现方式存在一个问题,那就是可能造成队头阻塞,如果死信队列中的队头的消息一直无法消费成功,那么就会阻塞整个队列,这时候即使排在他后面的消息过期需要处理了,那么也会被一直阻塞。


基于RabbitMQ的死信队列,可以实现延迟消息,非常灵活的实现定时关单,并且借助RabbitMQ的集群扩展性,可以实现高可用,以及处理大并发量。他的缺点第一是可能存在消息阻塞的问题,还有就是方案比较复杂,不仅要依赖RabbitMQ,而且还需要声明很多队列(exchange)出来,增加系统的复杂度

八、RabbitMQ插件


其实,基于RabbitMQ的话,可以不用死信队列也能实现延迟消息,那就是基于
rabbitmq_delayed._message_exchange:插件,这种方案能够解决通过死信队列实现延迟消息出现的消息阻塞问题。但是该插件从RabbitMQ的3.6.12开始支特的,所以对版本有要求。

这个插件是官方出的,可以放心使用,安装并启用这个插件之后,就可以创建x-delayed--message类型的队列了。


前面我们提到的基于死信队列的方式,是消息先会投递到一个正常队列,在TTL过期后进入死信队列。但是基于插件的这种方式,消息并不会立即进入队列,而是先把他们保存在一个基于Erlang开发的Mnesia数据库中,然后通过一个定时器去查询需要被投递的消息,再把他们投递到x-delayed-messagel队列中。

基于RabbitMQ插件的方式可以实现延迟消息,并且不存在消息阻塞的问题,但是因为是基于插件的,而这个插件支持的最大延长时间是(232)-1毫秒,大约49天,超过这个时间就会被立即消费。但是他基于RabbitMQ实现,所以在可用性、性能方便都很不错

九、Redis过期监听


很多用过Redisl的人都知道,Redis有一个过期监听的功能,
在redis.conf中,加入一条配置notify--keyspace-events Ex开启过期监听,然后再代码中实现一个KeyExpirationEventMessageListener,就可以监听key的过期消息了。这样就可以在接收到过期消息的时候,进行订单的关单操作。


这个方案不建议大家使用,是因为Redis官网上明确的说过,Redis并不保证Key在过期的时候就能被立即删除,更不保证这个消息能被立即发出。所以,消息延迟是必然存在的,随着数据量越大延迟越长,延迟个几分钟都是常事儿。

而且,在Redis5.0之前,这个消息是通过PUB/SUB模式发出的,他不会做持久化,至于Redis有没有接到,有没有消费成功,他不管。也就是说,如果发消息的时候,你的客户端挂了,之后再恢复的话,这个消息你就彻底丢失了。(在Redis5.0之后,因为引入了Stream,是可以用来做延迟消息队列的。)
 

十、Redis的zset


虽然基于Redis过期监听的方案并不完美,但是并不是Redis实现关单功能就不完美了,还有其他的方案。
 

我们可以借助Redis中的有序集合一zSet来实现这个功能。

zset是一个有序集合,每一个元素(member)都关联了一个score,可以通过score排序来取集合中的值。
我们将订单超时时间的时间戳(下单时间+超时时长)与订单号分别设置为score和member。这样redis会对zSet按照score延时时间进行排序。然后我们再开启redis扫描任务,获取"当前时间>score"的延时任务,


扫描到之后取出订单号,然后查询到订单进行关单操作即可。

使用redis zset来实现订单关闭的功能的优点是可以借助redis的持久化、高可用机制。避免数据丢失。但是这个方案也有缺点,那就是在高并发场景中,有可能有多个消费者同时获取到同一个订单号,一般采用加分布式锁解决,但是这样做也会降低吞吐型。
但是,在大多数业务场景下,如果幂等性做得好的,多个消费者取到同一个订单号也无妨。

十一、Redisson+Redis


上面这种方案看上去还不错,但是需要我们自己基于zset这种数据结构编写代码,那么有没有什么更加友好的方式?

有的,那就是基于Redisson。


Redisson;是一个在Redis的基础上实现的框架,它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。


 

Redisson中定义了分布式延迟队列RDelayedQueue,这是一种基于我们前面介绍过的zset结构实现的延时队列,它允许以指定的延迟时长将元素放到目标队列中。

其实就是在zSet的基础上增加了一个基于内存的延迟队列。当我们要添加一个数据到延迟队列的时候,redisson会把数据+超时时间放到zSet中,并且起一个延时任务,当任务到期的时候,再去zSet中把数据取出来,返回给客户端使用。

大致思路就是这样的,感兴趣的大家可以看一看RDelayedQueuel的具体实现。

基于Redisson的实现方式,是可以解决基于zset方案中的并发重复问题的,而且还能实现方式也比较简单,稳定性、性能都比较高。


总结

我们介绍了11种实现订单定时关闭的方案,其中不同的方案各自都有优缺点,也各自适用于不同的场景中。
那我们尝试着总结一下:

实现的复杂度上(包含用到的框架的依赖及部署):
Redisson>RabbitMQ插件>RabbitMQ死信队列>RocketMQ延迟消息≈Redis的zset>Redis过期监听≈kafkal时间轮>定时任务>Netty的时间轮>JDK自带的DelayQueue>被动关闭


方案的完整性:
Redisson≈RabbitMQ插件>kafkal时间轮>Redis的zset≈RocketMQ延迟消息≈RabbitMQ死信队列>Redis过期监听>定时任务>Netty的时间轮>JDK自带的DelayQueue>被动关闭


不同的场景中也适合不同的方案:

  • 自己玩玩:被动关闭
  • 单体应用,业务量不大:Nettyl的时间轮、JDK自带的DelayQueue、定时任务
  • 分布式应用,业务量不大:Redis过期监听、RabbitMQ死信队列、Redisl的zSet、定时任务
  • 分布式应用,业务量大、并发高:Redisson、RabbitMQ插件、kafka时间轮、RocketMQ延迟消息、定时任务
  • 业务量特别大:定时任务
     

总体考虑的话,考虑到成本,方案完整性、以及方案的复杂度,还有用到的第三方框架的流行度来说,个人比较建议优先考虑Redisson+Redis、RabbitMQ插件、Redis的zset、RocketMQ延迟消息等方案。


但是,在业务量特别大的时候,MQ其实并不适合:

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

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

相关文章

仓库管理系统哪个好用?看仓储出入库系统如何智慧管理库存-亿发

企业中的仓库扮演着至关重要的角色,负责产品的存储和分发。作为物流的重要节点,仓库不仅需要确保产品安全存放,还要保证及时的配送服务。同时,仓库还需要完成货物信息记录、库存管理和品质管理等任务。因此,仓储管理的…

DFS专题:二叉树的最大深度

力扣题目:二叉树的最大深度 题目链接: 104.二叉树的最大深度 题目描述 代码思路 设置两个变量,max来记录最大值,sum来记录路径的节点数量。利用dfs对二叉树进行搜索,遇到节点,则sum1;遇到叶子节点&#…

数据恢复如何工作?电脑最佳数据恢复软件分析

数据丢失是数字世界不合适的部分,迟早会影响许多计算机用户。 如果您不小心 #delete 了重要的 #file,可能很难找回它并造成不必要的压力。 点击发推文 幸运的是,即使您没有备份已删除的文件,PC的数据恢复软件也可以帮助您恢复已…

昂科烧录器支持Nuvoton新唐科技的低功耗微控制器M482SIDAE

芯片烧录行业领导者-昂科技术近日发布最新的烧录软件更新及新增支持的芯片型号列表,其中Nuvoton新唐科技的低功耗微控制器M482SIDAE已经被昂科的通用烧录平台AP8000所支持。 M482SIDAE以Arm Cortex-M4F为核心,是带有DSP指令集的高效能低功耗微控制器。其…

WPF Extended.Wpf.Toolkit 加载界面

1、NuGet 中安装 Extended.Wpf.Toolkit 。 2、在MainWindow.xaml中添加xmlns:tk"http://schemas.xceed.com/wpf/xaml/toolkit" 。 MainWindow.xaml 代码如下。 <Window x:Class"WPF_Extended_Wpf_Toolkit_Loading.MainWindow" xmlns"ht…

apipost、postman等工具上传图片测试flask、fastapi的文件api接口

参考&#xff1a;https://blog.csdn.net/qq_15821487/article/details/119354129 https://www.cnblogs.com/wyxjava/p/16076176.html 选择from-data&#xff0c;下拉选择file上传文件发送即可

【动态规划 区间dp 位运算】3117. 划分数组得到最小的值之和

本文涉及知识点 动态规划 区间dp 位运算 LeetCode3117. 划分数组得到最小的值之和 给你两个数组 nums 和 andValues&#xff0c;长度分别为 n 和 m。 数组的 值 等于该数组的 最后一个 元素。 你需要将 nums 划分为 m 个 不相交的连续 子数组&#xff0c;对于第 ith 个子数组…

vscode设置conda默认python环境,简单有效

本地conda 可能安装了各种环境&#xff0c;默认的vscode总是base环境&#xff0c;这时你想要在vscode调试python代码&#xff0c;使用默认的环境没有安装对应的包就会遇到报错解决这个问题的方法很简单ctrlshiftp 调出命令面板 再输入 select interpreter , 选择 python 选择解…

在Spring Boot中使用POI完成一个excel报表导入数据到MySQL的功能

最近看了自己玩过的很多项目&#xff0c;忽然发现有一个在实际开发中我们经常用到的功能&#xff0c;但是我没有正儿八经的玩过这个功能&#xff0c;那就是在Spring Boot中实现一个excel报表的导入导出功能&#xff0c;这篇博客&#xff0c;主要是围绕excel报表数据导入进行&am…

一例Mozi僵尸网络的挖矿蠕虫分析(workminer)

概述 这是一个Linux平台的挖矿蠕虫&#xff0c;使用了go和C混合编译而成&#xff0c;主要通过爆破SSH口令进行传播&#xff0c;属于Mozi僵尸网络。其中GO代码负责SSH相关的爆破传播&#xff0c;以及对Config的处理&#xff0c;C代码则负责处理加入Mozi P2P网络&#xff0c;拉取…

c++11 标准模板(STL)本地化库 - 平面类别(std::collate) - 定义字典序比较和字符串的散列(二)

本地化库 本地环境设施包含字符分类和字符串校对、数值、货币及日期/时间格式化和分析&#xff0c;以及消息取得的国际化支持。本地环境设置控制流 I/O 、正则表达式库和 C 标准库的其他组件的行为。 平面类别 定义字典序比较和字符串的散列 std::collate 类 std::collate 封…

Redis的Stream 和 实现队列的方式【List、SortedSet、发布订阅、Stream、Java】

Redis队列与Stream、Redis 6多线程详解 Redis队列与StreamStream总述常用操作命令生产端消费端单消费者消费组消息消费 Redis队列几种实现的总结基于List的 LPUSHBRPOP 的实现基于Sorted-Set的实现PUB/SUB&#xff0c;订阅/发布模式基于Stream类型的实现与Java的集成 消息队列问…

MySQL高级(索引-性能分析-profile)

show profiles 能够在做SQL优化时帮助我们了解时间都耗费到哪去了。通过 have_profiling参数&#xff0c;能够看到当前MySQL 是否支持 profile 操作&#xff1a; select have_profiling 默认 profiling 是关闭的 select profiling; 可以通过 set 语句在 session / global 级…

【python】flask操作数据库工具SQLAlchemy,详细用法和应用实战

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

萌新_1 环境安装(基于QQNT框架 Python Flask)

遇到问题加QQ群聊 群主在线解答 点击加入群聊【星辰开发】 一&#xff1a;安装QQ 目前为开发&#xff0c;推荐都安装到一台电脑上 直接安装到本地windows电脑&#xff0c; 优点方便开发 一键安装 Windows 用户一键安装方案 https://github.com/super1207/install_llob/rel…

从零到一品牌电商私域流量代运营规划方案

【干货资料持续更新&#xff0c;以防走丢】 从零到一品牌电商私域流量代运营规划方案 部分资料预览 资料部分是网络整理&#xff0c;仅供学习参考。 PPT共50页&#xff08;完整资料包含以下内容&#xff09; 目录 私域运营方案&#xff1a; 一、项目背景与目标 - 开创数智化…

kaggle电子邮件分类xgboost建模可视化模型评估混淆矩阵范例

目录 概述 依赖环境 代码解读 库的导入 数据读取 扇形图可视化统计 词云图可视化 分布条形图可视化 数据预处理 划分数据集 模型训练 模型预测和评估 ROC曲线评估 混淆矩阵评估 多维度交叉评估 配套源码和数据集 xgboost邮件分类配套数据集和源码下载地址 概述…

RK3568 学习笔记 : u-boot 千兆网络功能验证

前言 开发板型号&#xff1a; 【正点原子】 的 RK3568 开发板 使用 虚拟机 ubuntu 20.04 编译 RK3568 Linux SDK&#xff0c;生成镜像&#xff0c;烧写后&#xff0c;Linux 系统正常启动 开启后可以使用 CTRLC 进入 u-boot 本篇验证一下 u-boot 下网络功能 【正点原子】 rk…

OpenHarmony 视图缩放组件—subsampling-scale-image-view

简介 深度缩放视图&#xff0c;图像显示&#xff0c;手势平移缩放双击等 效果图&#xff08;旋转、缩放、平移&#xff09; 下载安装 ohpm install ohos/subsampling-scale-image-view OpenHarmony ohpm 环境配置等更多内容&#xff0c;请参考如何安装 OpenHarmony ohpm 包 使…

【Linux C | 多线程编程】线程同步 | 信号量(无名信号量) 及其使用例子

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…