订单到期关闭如何实现?

目录

一、被动关闭

二、定时任务

三、JDK自带的DelayQueue

四、Netty的时间轮

五、Kafka的时间轮

六、RocketMQ延迟消息

七、RabbitMQ死信队列

八、RabbitMQ插件

九、Redis过期监听

十、Redis的Zset

十一、Redisson


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

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

  1. 被动关闭(不推荐)
  2. 定时任务(推荐,适合时间精度要求不高的场景)
  3. DelayQueue(不推荐,基于内存,无法持久化)
  4. 时间轮(不推荐,基于内存,无法持久化)
  5. kafka(MQ 方案不推荐,大量无效调度)
  6. RocketMQ延迟消息(MQ 方案不推荐,大量无效调度)
  7. RabbitMQ死信队列(MQ 方案不推荐,大量无效调度)
  8. RabbitMQ插件(MQ 方案不推荐,大量无效调度)
  9. Redis过期监听(不推荐,容易丢消息)
  10. Redis的ZSet(不推荐,可能会重复消费)
  11. Redisson(推荐,可以用)

实现的复杂度上(包含用到的框架的依赖及部署):

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

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

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

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

但是,如果考虑到订单到期关闭的业务特点,在订单量特别大的时候,MQ其实并不适合。

一、被动关闭

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

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

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

还有一个缺点,那就是需要在用户的查询过程中进行写的操作,一般写操作都会比读操作耗时更长,而且有失败的可能,一旦关单失败了,就会导致系统处理起来比较复杂。

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

二、定时任务

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

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

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

  1. 时间不精准。一般定时任务基于固定的频率、按照时间定时执行的,那么就可能会发生很多订单已经到了超时时间,但是定时任务的调度时间还没到,那么会导致这些订单的实际关闭时间要比应该关闭的时间晚一些。
  2. 无法处理大订单量。定时任务的方式是会把本来比较分散的关闭时间集中到任务调度的那一段时间,如果订单量比较大的话,那么就可能导致任务执行时间很长,整个任务的时间越长,订单被扫描到时间可能就很晚,那么就会导致关闭时间更晚。
  3. 对数据库造成压力。定时任务集中扫表,这会使数据库IO在短时间内被大量占用和消耗,如果没有做好隔离,并且业务量比较大的话,就会影响到底线上的正常业务。
  4. 分库分表问题。订单系统,一旦订单量大就可能会考虑分库分表,在分库分表中进行全表扫描,这是一个极不推荐的方案。

三、JDK自带的DelayQueue

有这样一种方案,他不需要借助任何外部的资源,直接基于应用自身就能实现,那就是基于JDK自带的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类,位于kafka.utils.timer包中。基于Kafka的时间轮同样可以得到O(1)时间复杂度,性能上还是不错的。

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

六、RocketMQ延迟消息

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

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

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

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

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

如果我们业务上的关单时长刚好和RocketMQ延迟消息支持的时长相匹配的话,那么是可以基于RocketMQ延迟消息来实现的。否则,这种方式并不是最佳的。(但是在RocketMQ 5.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-message队列中。

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

九、Redis过期监听

很多用过Redis的人都知道,Redis有一个过期监听的功能。

在redis.conf中加入一条配置 notify-keyspace-events Ex 开启过期监听,然后再代码中实现一个KeyExpirationEventListener,就可以监听key的过期消息了。

这样就可以在接收到过期消息的时候,进行订单的关单操作。

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

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

十、Redis的Zset

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

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

zset是一个有序集合,每一个元素(member)都关联了一个 score,可以通过 score 排序来取集合中的值。

我们将订单超时时间的时间戳(下单时间+超时时长)与订单号分别设置为 score 和 member。这样redis会对 zset按照score延时时间进行排序。然后我们再开启redis扫描任务,获取“当前时间 > score”的延时任务,扫描到之后取出订单号,然后查询到订单进行关单操作即可。

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

但是,在大多数业务场景下,如果幂等性做得好的,多个消费者取到同一个订单号也无妨。

十一、Redisson

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

有的,那就是基于Redisson。

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

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

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【STM32单片机_(HAL库)】3-2-1【中断EXTI】【电动车报警器项目】震动点灯

1.硬件 STM32单片机最小系统LED灯模块震动传感器模块 2.软件 exti驱动文件添加GPIO常用函数中断配置流程main.c程序 #include "sys.h" #include "delay.h" #include "led.h" #include "exti.h"int main(void) {HAL_Init(); …

【CAN-IDPS】汽车网关信息安全要求以及实验方法

《汽车网关信息安全技术要求及试验方法》是中国的一项国家标准,编号为GB/T 40857-2021,于2021年10月11日发布,并从2022年5月1日起开始实施 。这项标准由全国汽车标准化技术委员会(TC114)归口,智能网联汽车分会(TC114SC34)执行,主管部门为工业和信息化部。 该标准主要…

使用gitee存储项目

gitee地址:Gitee - 基于 Git 的代码托管和研发协作平台 创建gitee远程仓库 将远程仓库内容拉取到本地仓库 复制下面这个地址 通过小乌龟便捷推送拉取代码:https://blog.csdn.net/m0_65520060/article/details/140091437

如何通过观测云实现AIOps突破?

在当今信息技术迅猛发展的浪潮中,企业正置身于一个日益复杂化的 IT 环境,并面临着数据量的爆炸性增长。智能运维(AIOps),作为 IT 运维管理领域的革新者,融合了大数据和机器学习技术,致力于对 IT…

CV每日论文--2024.7.25

1、Diffusion Models for Monocular Depth Estimation: Overcoming Challenging Conditions 中文标题:单目深度估计的扩散模型:克服具有挑战性的条件 简介:本文提出了一种新颖的方法,旨在解决单张图像深度估计任务中具有挑战性的、超出分布范…

有关应用层面试题有关库的思维导体

面试题目: TCP通信中3次握手和四次挥手? 答: 第一次握手:客户端发送SYN包(SYN1, seq0)给服务器,并进入SYN_SENT状态,等待服务器返回确认包。第二次握手:服务器接收到S…

LMDeploy 量化部署实践闯关任务

一、LMDeploy量化介绍 1.LMDeploy部署模型的优势 LMDeploy实现了高效的推理、可靠的量化、卓越的兼容性、便捷的服务以及有状态的推理。 相比于vllm具有领先的推理性能: LMDeploy也提供了大模型量化能力:主要包括KV Cache量化和模型权重量化。 LMDepl…

二叉树的经典OJ题

前言 Helllo,今天,博主将要带领大家来深度解析几道经典的二叉树OJ题,来巩固我们前面学过的二叉树知识,我们在进行二叉树练习的时候,还是要对二叉树有较为深入的认识,所以新来的小伙伴,博主强烈推荐可以先去…

MyBatis[进阶]

大纲: 动态SQL查询 留言板 1. 动态SQL 1.1 <if> 我们都注册过一些信息,有的信息是非必填项,改如何实现呢? 这个时候就需要使⽤动态标签来判断了 ⽐如添加的时候性别gender为⾮必填字段&#xff0c;具体实现如 下&#xff1a; 注解: 如果性别为空: 如果性别不为空:…

HDU1159——通用子序列,HDU1160——FatMouse的速度、HDU1165——艾迪的研究 II

HDU1159——通用子序列 题目描述 问题 - 1159 (hdu.edu.cn) 问题描述 给定序列的子序列是给定的序列&#xff0c;其中遗漏了一些元素&#xff08;可能没有&#xff09;。给定一个序列 X <x1&#xff0c; x2&#xff0c; ...&#xff0c; xm>如果存在一个严格递增的 X …

「字符串」详解AC自动机并实现对应的功能 / 手撕数据结构(C++)

目录 前置知识 概述 核心概念&#xff1a;fail指针 作用 构建 图示 Code 成员变量 创建销毁 添加词库 文本扫描 复杂度 Code 前置知识 在此前&#xff0c;你应该首先了解trie树&#xff08;字典树&#xff09;的概念&#xff1a; 「字符串」详解Trie&#xff0…

C语言贪吃蛇之BUG满天飞

C语言贪吃蛇之BUG满天飞 今天无意间翻到了大一用C语言写的贪吃蛇&#xff0c;竟然还标注着BUG满天飞&#xff0c;留存一下做个纪念&#xff0c;可能以后就找不到了 /* 此程序 --> 贪吃蛇3.0 Sur_流沐 当前版本&#xff1a; Bug满天飞 */ #include<stdio.h> #includ…

Chat App 项目之解析(二)

Chat App 项目介绍与解析&#xff08;一&#xff09;-CSDN博客文章浏览阅读76次。Chat App 是一个实时聊天应用程序&#xff0c;旨在为用户提供一个简单、直观的聊天平台。该应用程序不仅支持普通用户的注册和登录&#xff0c;还提供了管理员登录功能&#xff0c;以便管理员可以…

xlsx表格-A列的值需要从C列中匹配到然后输出C列旁边D列的值,怎么写公式?

公式&#xff1a; IFERROR(VLOOKUP(A1, C:D, 2, FALSE), "") 解释&#xff1a; 在VLOOKUP函数中&#xff0c;2表示要返回的列的索引。具体来说&#xff0c;VLOOKUP函数的语法如下&#xff1a; VLOOKUP(lookup_value, table_array, col_index_num, [range_lookup])…

微服务的保护

一、雪崩问题及解决方案 1.雪崩问题 微服务之间&#xff0c;一个微服务依赖多个其他的微服务。当一个微服务A依赖的一个微服务B出错时&#xff0c;微服务A会被阻塞&#xff0c;但其他不依赖于B的微服务不会受影响。 当有多个微服务依赖于B时&#xff0c;服务器支持的线程和并…

入门 - Vue中使用axios原理分析及解决前端跨域问题

1. 什么是Axios&#xff1f; Axios&#xff08;ajax i/o system&#xff09;&#xff0c;是Vue创建者主推的请求发送方式&#xff0c;因其简单的配置与良好的性能被前端爱好者所喜爱。众所周知&#xff0c;在进行网页设计时经常需要从后端拿数据&#xff0c;在Web应用初期会将…

python之matplotlib (1 介绍及基本用法)

介绍 matplotlib是Python中的一个绘图库&#xff0c;它提供了一个类似于 MATLAB 的绘图系统。使用matplotlib你可以生成图表、直方图、功率谱、条形图、错误图、散点图等。matplotlib广泛用于数据可视化领域&#xff0c;是 Python 中最著名的绘图库之一。 同样matplotlib的安…

golang实现一个简单的rpc框架

前言 RPC在分布式系统中经常使用&#xff0c;这里写一个简单的demo实践一下。 code 先生成 go.mod 文件 go mod init rpc-try01定义方法 package model// Args 是 RPC 方法的参数结构体 type Args struct {A, B int }// Arith 定义了一个简单的算术服务 type Arith struct{…

:class的用法及应用

参考小满视频 在同一个标签中&#xff0c;class只能有一个&#xff0c;:class也只能有一个 :class的用法 1. :class “非响应式的变量”&#xff08;一般不使用&#xff0c;和写死了一样&#xff09; const a "style1" <span :class"a"></spa…

python-逆序数(赛氪OJ)

[题目描述] 在一个排列中&#xff0c;如果一对数的前后位置与大小顺序相反&#xff0c;即前面的数大于后面的数&#xff0c;那么它们就称为一个逆序。一个排列中逆序的总数就称为这个排列的逆序数。比如一个元素个数为 4 的数列&#xff0c;其元素为 2,4,3,1&#xff0c;则 (2,…