订单超时自动关闭的本质其实是一种延时的功能实现,具体实现方式有很多种,但是我们方式的选择是需要结合业务场景的,没有更好的方案,只有更适合的方案,所以我们必须要结合自己的实际业务,以及业务的后续发展,以及时间的紧迫程度进行选择。
在这里,我会将每一种方式的实现步骤,优缺点,以及适用场景都会给大家说清楚,供大家选择。
1. 消息队列模式实现
消息队列是一种解耦合、异步处理的方式,非常适合用于实现订单超时关闭功能。在订单创建时,向消息队列发送一个定时消息,消息的延时时间设置为订单的超时时间。如果订单在超时时间内未完成支付,消息队列将触发关闭订单的操作。
实现步骤:
- 在订单创建时,向消息队列发送一个定时消息,消息的延时时间设置为订单的超时时间。
- 消息队列在消息到期时触发关闭订单的操作。
- 关闭订单后,执行相应的后续处理逻辑,如释放库存、通知用户等。
优点:
- 解耦合:订单创建和关闭操作通过消息队列进行解耦合,提高了系统的可扩展性和可靠性。
- 异步处理:订单关闭操作在消息到期时异步执行,不会阻塞主业务逻辑。
缺点:
- 常用的消息中间件对延迟的时间是有一定规定的,比如RabbitMQ最大延迟时间是2的32次方毫秒,RocketMQ的消息延时只有特定的 18个级别,灵活度不够。
- 还有我们如果使用的是开源版本的RocketMQ,注意是没有删除延时消息的功能,因此需要对每条消息都做超时判断,增加了很多无效的数据处理。
- 强依赖消息中间件,所以我们需要保证MQ的稳定以及持续消费功能,防止消息丢失和积压问题。
2. Kafka的时间轮
这种方案也是比较优秀的,如果大家的项目中,采用的消息中间件是Kafka,又想利用中间件处理延迟队列的话,可以采用这种方式,我没有试过,但是大家可以看下这篇文章,感觉还是比较透彻的,后续我也会给大家专门做一个实践。
Kafka的时间轮实现延迟队列
3. 定时轮询模式
具体实现细节就是我们通过一些调度平台(XXL-Job,SchedulerX)来实现定时执行任务,任务就是去扫描所有到期的订单,然后执行关单动作。
优点:
- 实现起来很容易,基于Timer、ScheduledThreadPoolExecutor、或者像xxl-job这类调度框架都能实现
缺点:
- 时间不精准。 一般定时任务基于固定的频率、按照时间定时执行的,那么就可能会发生很多订单已经到了超时时间,但是定时任务的调度时间还没到,那么就会导致这些订单的实际关闭时间要比应该关闭的时间晚一些。
- 无法处理大订单量。 定时任务的方式是会把本来比较分散的关闭时间集中到任务调度的那一段时间,如果订单量比较大的话,那么就可能导致任务执行时间很长,整个任务的时间越长,订单被扫描到时间可能就很晚,那么就会导致关闭时间更晚。
- 对数据库造成压力。 定时任务集中扫表,这会使得数据库IO在短时间内被大量占用和消耗,如果没有做好隔离,并且业务量比较大的话,就可能会影响到线上的正常业务。
- 分库分表问题。 订单系统,一旦订单量大就可能会考虑分库分表,在分库分表中进行全表扫描,这是一个极不推荐的方案。
使用场景:
定时任务的方案,适合于对时间精确度要求不高、并且业务量不是很大的场景中。如果对时间精度要求比较高,并且业务量很大的话,这种方案不适用。
4. 懒延时模式
业务上的被动关闭是一种简单的解决方案。在订单创建后,系统不主动关闭订单,而是等待用户来访问订单。当用户访问订单时,系统检查订单的支付状态和时间,如果订单未支付且已超时,则进行关单操作。
实现步骤:
- 在订单创建后,不执行主动关单操作。
- 当用户访问订单时,检查订单的支付状态和时间。
- 如果订单未支付且已超时,进行关单操作并提示用户。
优点:
实现简单,基本不需要开发定时关闭功能。
缺点:
- 如果用户一直不来查看订单,会导致数据库中存在大量未关闭的脏数据。
- 需要在用户查询过程中进行写操作,可能影响性能和稳定性。
- 影响查询之外的业务(如:统计、库存),影响查询效率。
5. Redis方案
方式一:过期监听
Redis 是一个高性能的 KV 数据库,除了用作缓存以外,其实还提供了过期监听的功能。其本质是注册一个 listener,利用 redis 的发布订阅,当 key 过期时,发布过期消息(key)到 Channel 的指定Topic中。
实现步骤:
- 在 redis.conf 中,配置 notify-keyspace-events Ex 即可开启此功能。
- 在代码中继承 KeyspaceEventMessageListener,实现 onMessage 就可以监听过期的数据量。
- 在实际的业务中,我们可以将订单的过期时间设置比如 30 分钟,然后放入到 redis。30 分钟之后,就可以消费这个 key,然后做一些业务上的后置动作,比如检查用户是否支付。
优点:
由于 redis 的高性能,所以我们在设置 key,或者消费 key 时,速度上是可以保证的。
缺点:
- 我们得要先知道Redis删除过期key的策略,分别有两种,一种是定期删除,一种是惰性删除。定期删除就是过一段时间,就会随机抽取一批设置了过期时间的key,检查是否过期,过期则清理,这样限制了执行时长避免影响业务,惰性删除就是不主动去删除过期key,每次访问的时候才会进行删除。所以你看出来了吗,Redis的key删除策略是不精准的,订单量大的话弊端就会显露出来。
- 还有就是Redis过期通知也是不可靠的,Redis在过期通知的时候,如果应用正好重启了,那么就有可能通知事件就丢了,会导致订单一直无法关闭,有稳定性问题。
所以总结下来,redis 的过期订阅相比于其他方案没有太大的优势,在实际生产环境中,用得相对较少。
方式二:Redisson分布式延迟队列
Redisson 除了提供我们常用的分布式锁外,还提供了一个分布式延迟队列 RDelayedQueue,是一种基于 Redis Zset 结构的延时队列实现。DelayQueue 中有一个名为 timeoutSetName 的有序集合,其中元素的 score 为投递时间戳。
DelayQueue 会定时使用 zrangebyscore 扫描已到投递时间的消息,然后把它们移动到就绪消息列表中。
它的稳定性以及可靠性还是不错的,在保证Redis稳定地情况下,可以推荐使用,具体实现可以看下这篇文章Redisson实现延迟队列。后续我也会给大家做下示例。
但是要注意:
- Redisson的的RDelayedQueue是基于Redis实现的,而Redis本身并不保证数据的持久性。如果Redis服务器宕机,那么所有在RDelayedQueue中的数据都会丢失。因此,我们需要在应用层面进行持久化设计,例如定期将RDelayedQueue中的数据持久化到数据库。
- 随着RDelayedQueue中数据量的增长,后台线程的检查频率和处理速度可能会成为瓶颈,影响系统的并发性能。因此,我们需要对后台线程进行适当的优化,例如使用多线程进行处理,或者调整检查频率。
处理思路:
- 合理设计延迟时间:在设计延迟任务时,我们应该根据实际需求来合理设置延迟时间,避免设置过长的延迟时间导致内存占用过高。
- 利用Redisson的的持久化机制:Redisson提供了多种持久化机制,例如RDB和AOF。我们可以利用这些机制来定期将RDelayedQueue中的数据持久化到磁盘,以确保数据的安全性。
- 监控和告警:我们应该注意监控RDelayedQueue的状态和性能,例如检查队列的长度、后台线程的处理速度等。当发现异常情况时,应该及时告警并进行处理。
6. 利用数据库的定时任务功能
一些数据库(如MySQL)提供了定时任务功能,我们可以利用它来实现订单超时关闭。在订单创建时,记录订单的创建时间和超时时间。然后,通过数据库的定时任务定期检查订单的支付状态,如果订单未支付且已超时,则将其状态设置为关闭。
实现步骤:
- 在订单创建时,记录订单的创建时间和超时时间。
- 设置数据库的定时任务,定期检查对应订单的支付状态。
- 如果订单未支付且已超时,将其状态设置为关闭,并执行后续处理逻辑。
优点:
- 利用数据库的内置功能,无需额外开发定时任务系统。
- 定时任务与业务逻辑紧密集成,方便管理和维护。
缺点:
- 这种就纯粹依靠数据库来实现了,需要用到数据库的事件功能,如果在数据库集群的情况下,我们需要评估这种方式的可行性和复杂性,同时当数据量比较大的时候,对数据库的压力也是比较大的。同时异常情况的可追溯性不强,所以不推荐使用。
7. JDK延迟队列
JDK延时队列DelayQueue是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素。
优点:
- 不需要借助任何外部的资源,直接基于应用自身就能实现。
缺点:
- 只适合在单机场景、并且数据量不大的场景中使用,如果涉及到分布式场景,那还是不建议使用。
- 订单量太大的话,可能会导致OOM的问题。
- DelayQueue是基于JVM内存的,一旦机器重启了,里面的数据就都没有了。
使用场景:
只适合demo测试。
总结
经过咱们上面的讨论,其实在实际开发中,我们可以这样选择:
- 如果项目中使用的消息中间件是RabbitMQ或RocketMQ,对延迟精度要求较高,业务量较大,不想对Redis有太大压力的,推荐使用方式1。
- 如果项目中使用的消息中间件是Kafka,对延迟精度要求较高,业务量较大,不想对Redis有太大压力的,推荐使用方式2。
- 如果项目中没有使用消息中间件,后续也不打算引入,业务量不大,对消息丢失有一定的容忍,可以采用Redisson策略。
- 如果项目中不想用中间件或者Redis来处理,同时业务量不大,对延迟有一定的容忍,简单起见可以采用定时轮询策略。