在具有支付购买功能的程序中,无论是电商、售票、挂号,基本进行支付下订单的操作后,都会有规定时间内进行支付,如果超时的话,订单就会自动关闭,这种功能就是订单延迟关闭的功能
订单延时关闭的功能主要是为了提高用户体验和操作灵活性,给用户预留足够的时间考虑或完成支付过程,同时也为商家提供一定程度上的库存管理和订单管理灵活性
实现这种功能是很多种方式,先来介绍常见的几种
定时任务扫描
这种是定时任务执行,然后去查询数据库中支付但还没有付款状态的订单,并判断该订单的支付时间有没有超过指定的时间,如果超过了,将订单状态修改为关闭状态,这种其实在小项目是可以,但一旦请求量或者订单量高是,就不适用了
- 性能问题:当订单量非常大时,定时任务需要扫描整个数据库或大量记录来检查哪些订单需要被关闭。这会导致数据库压力增大,尤其是在高峰时段,可能会影响数据库的响应速度和整体的系统性能
- 扩展性问题:随着业务量的增长,定时任务单一的执行方式可能会遇到瓶颈。如果系统设计没有考虑到分布式环境,增加更多的服务器和数据库分片来处理更多的订单将会变得复杂且困难
- 实时性问题:定时任务通常按照预设的时间间隔运行,这可能会导致订单关闭的操作有所延迟。例如,如果定时任务每小时运行一次,那么在某些情况下订单可能会在实际到期时间后的一小时内才被关闭,这影响了系统的实时性和用户体验
- 资源浪费:定时任务扫描整数据库或大量订单记录,即使大部分订单都不需要被关闭,也会消耗大量的计算资源和数据库IO,这在资源利用率上是一种浪费
- 难以管理和维护:定时任务的管理和维护可能会变得复杂,特别是当需要调整任务的执行频率或者处理逻辑时。随着业务规则的变化,更新和维护定时任务可能会导致代码复杂度增加和维护成本提高
RocketMQ
Apache RocketMQ 是一个分布式消息中间件和流计算平台,具有高吞吐量、高可用性、可扩展性和低延迟等特点。它广泛用于处理大规模消息的传递,支持多种消息通信模式,包括发布/订阅、请求/响应等。
RocketMQ 原生支持延迟消息,允许你在发送消息时指定延迟级别,这使得实现延迟队列变得非常简单。消息会在指定的延迟时间后被投递到目标队列,无需额外的定时任务或复杂的逻辑
在业务方设置消息的处理器,当到达指定时间后就会触发消息的消费,RocketMQ为了保证消息的可靠性传递,可能会多次发送消息,所以消费方要做好幂等性的保护措施
在4.x之前,只能选择给定的级别,来对应的时间级别。包括 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
在5.x之后,不用再只能选择级别,而是可以自定义配置时间的延迟
关于4.x和5.x的区别和改进,可跳转到此文章详细查看
弥补延时消息的不足,RocketMQ 基于时间轮算法实现了定时消息!-51CTO.COM
Redis
redis中存在过期消息监听的功能,比如在redis中设置带有过期时间的键值,接着在业务方设计一个 监听处理器,来监听此键值,当指定的时间到期后,会触发过期事件,监听器就会监听到这个事件,来执行逻辑。
但redis这种过期监听事件是存在问题的
-
键的过期不准确 键的过期事件发布时机并不是当这个键的过期时间到了之后就发布,而是redis把这个键真正被删除之后才会发布。而redis过期键是由两种清除策略。
-
惰性清除 当这个键过期之后,再去访问这个键时,才会被真正清除
-
定时清除 redis会定期检查一部分的键,如果有键过期了,就会被清除
-
-
消息丢失 redis过期监听实现的发布订阅模式没有持久化机制,当消息发布到某个channel之后,如果没有客户端订阅这个channel,那么这个消息就丢了。这个问题在5.x之前存在,而在5.x之后推出了Stream,解决了延迟消息持久化的问题
-
优势 Stream 旨在处理消息流,并且可以被视为一个日志类型的数据结构,其中的每个条目都包含一个或多个键值对。使用 Stream,可以实现类似于 Apache Kafka 的消息队列功能
-
消息持久化:与 Redis 的其他数据类型一样,Stream 支持数据持久化,确保即使在服务器宕机或重启的情况下,消息也不会丢失。这对于需要可靠消息传递的应用来说非常关键。
-
消息顺序:Stream 保证消息的顺序,每条消息都被分配了一个唯一的序列号(ID),这使得消费者可以准确地按照消息产生的顺序处理消息。
-
消费组:Stream 支持消费组的概念,允许多个消费者分摊消息的处理工作。每个消费组内的消费者可以跟踪哪些消息已被处理,从而实现消息的负载均衡以及容错处理。
-
消息确认:消费者处理完消息后,可以对消息进行确认。这一机制确保了每条消息至少被处理一次,防止消息遗失。
-
阻塞读取:消费者可以使用阻塞读取模式来监听新消息,这意味着如果当前没有可用消息,消费者可以等待直到有新消息到来。
-
-
发布订阅模式中消息消费只有广播模式 如果有多个实例同时监听某个键的过期事件,当事件发布时,他们都会得到监听到,这时就有可能造成重复消费,需要有幂等性的保护机制
-
监听范围广 只能指定/不指定监听哪个库的所有的key,导致所有的key发生了事件都会被通知给消费者。没有针对性。
生产环境中其实不建议使用redis来实现延迟队列的,而Redisson作为分布式锁的头号No1,除了提供优秀的分布式锁工具外,还提供了基于redis,自己封装了功能,来实现延迟队列功能,并且解决了上述redis本身实现延迟队列的问题
使用 Redisson 的 RDelayedQueue 实现延迟队列
Redisson: Easy Redis Java client with features of In-Memory Data Grid
RDelayedQueue
是 Redisson 提供的一个接口,用于实现延迟队列的功能。它允许将元素延迟一段时间后再被消费。实现延迟队列的步骤如下:
-
创建 RDelayedQueue:首先需要创建一个普通的队列(例如
RQueue
或RBlockingQueue
),然后使用这个队列创建一个RDelayedQueue
实例。 -
添加延迟元素:通过
RDelayedQueue
的offer
方法添加元素,并指定延迟时间。元素将在指定的延迟时间后自动转移到原始队列中,随后可被消费。 -
消费元素:从原始队列中消费元素。如果是
RBlockingQueue
,消费者可以阻塞等待直到元素可用。
Redisson 延迟队列的特点
-
使用
RDelayedQueue
时,延迟的元素实际上是首先存储在 Redis 中的一个内部列表中,然后在到期后转移到目标队列。因此,需要保持 Redisson 实例运行,以便它可以处理延迟元素的转移 -
当 Redisson 客户端重启时,
RDelayedQueue
的状态会被自动恢复,因为其状态是持久化在 Redis 中的。这意味着即使应用重启,延迟队列的功能也不会受到影响
示例
// 创建 Redisson 客户端实例
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
// 获取一个 RBlockingQueue 实例
RBlockingQueue<String> queue = redisson.getBlockingQueue("myQueue");
// 使用 RBlockingQueue 创建 RDelayedQueue
RDelayedQueue<String> delayedQueue = redisson.getDelayedQueue(queue);
// 将一个元素添加到延迟队列中,延迟 10 秒钟
delayedQueue.offer("myElement", 10, TimeUnit.SECONDS);
// 在其他线程或者程序中,从 RBlockingQueue 中消费元素
String element = queue.take(); // 这会阻塞等待直到元素可用
// 关闭 Redisson 客户端
redisson.shutdown();