延时任务方案
前言
原本以为延时操作的方案就消息延时队列最合适, 却忽略了业务和架构背景. 因此最近也查询学习了几种方案以及相对合适的使用情况
正文
先看一下大语言模型的回答:
在Spring Boot中实现延时关闭订单,常见的方法有以下几种:
1. **定时任务(@Scheduled注解)**:
通过Spring的`@Scheduled`注解可以轻松实现定时任务。这种方案会周期性地扫描数据库,检查那些生成后还未支付的订单。如果订单在设定的延时时间(例如30分钟)内仍未支付,系统则会自动取消该订单。这种方法的优点是实现简单,但缺点是可能会有性能问题,特别是当系统负载较大时,因为需要频繁地访问数据库。
2. **延迟队列(如RabbitMQ)**:
延迟队列可以有效地处理延时任务。在订单生成时,将订单ID发送到RabbitMQ的消息队列中,并设置一定的延迟时间。当延迟时间到达后,队列中的消息会被消费,从而触发订单的取消操作。RabbitMQ的延迟队列功能可以很好地满足这种需求。不过,这种方法需要额外的配置和部署消息中间件。
3. **Redis过期事件**:
利用Redis的键过期事件特性,当订单生成时,在Redis中设置一个键,并设置对应的过期时间。当键过期时,Redis会触发一个事件,系统可以通过监听这个事件来执行订单取消操作。这种方法可以实现准实时的订单取消,并且性能较好,因为Redis的过期事件处理是高效的。
4. **定时轮询结合Redis或数据库**:
通过定时轮询结合Redis或数据库来实现延时关闭订单的功能。系统定时查询数据库或Redis中的订单状态,当发现订单在设定时间内未被支付时,执行关闭订单的操作。这种方法的缺点是轮询可能会引入性能问题,并且时间精度可能不够高。
5. **使用JDK的延迟队列**:
JDK提供的延迟队列(如`DelayedQueue`)也可以用于实现延时关闭订单。通过将订单信息放入延迟队列,并设置对应的延迟时间,当队列中的元素到期时,会触发订单的取消操作。
6. **使用Spring的事件驱动模型**:
在Spring框架中,还可以通过事件驱动模型来实现延时关闭订单。当订单生成时,发布一个事件,然后通过配置一个事件监听器来在指定的时间后处理这个事件,从而实现订单的自动取消。
综上,选择哪种方案取决于具体的业务需求、系统架构、以及对性能、精确时间控制等方面的考量。
它给出的推荐都还是比较合理的, 基本也符合现有的方案, 那这边就结合的在具体描述一下:
1. 定时任务
- 优点: 方便, 简单 ,可以直接用spring,jdk自带功能, 或者引入三方框架 即可使用(常见的: Timer, ScheduledExecutorService ,Scheduled, Quartz, xxl-job)
- 缺点: 时间精度差; 定时循环查询对系统, 数据库压力大; 无法处理大量订单
- 适用场景: 对时间精确度要求不高、并且业务量不是很大的情况
2. DelayQueue
- 优点: 同样很方便, 简单 ,DelayQueue是Java中的一个无界阻塞队列. jdk自带, 直接使用,
- 缺点: 一般需要配合while(true)使用; DelayQueue是基于JVM内存的 ,量大可能造成导致OOM , 同时重启后内容会丢失; 集群情况很难把控
- 适用场景: 适合在单机场景、并且数据量不大的场景
3. redis
(1). 过期方案
- 不建议使用, 因为redis的key过期删除并不保证会立即删除, 网上也有很多大佬测试过了, 尤其数据量大的时候 ,延迟会更高. 而且它通知使用的是 发布/订阅 模式, 并不能保证消息一定被消费
- 不过现在的新版本Redis 5.0之后,引入了Stream,是可以用来做延迟消息队列的, 这个可以考虑
(2). Redis的zset
- 这种方式则是利用它自身数据结构score的特点, 将延时时间的时间戳(当前时间+超时时长)与任务号分别设置为 score 和 member. 这样redis会对zset按照score延时时间进行排序, 然后我们再开启redis扫描任务(ZSCAN),获取"当前时间 > score"的延时任务,扫描到之后取出member,然后查询操作即可
- 优点: 可以借助redis的持久化、高可用机制, 避免数据丢失
- 缺点: 高并发场景中,有可能有多个消费者同时获取到同一个任务,一般采用加分布式锁解决,但是这样做也会降低吞吐型
(3). Redisson + Redis
- 这个就相当于zset的基础上增加了一个基于内存的延迟队列, 当我们要添加一个数据到延迟队列的时候,redisson会把数据+超时时间放到zset中,并且起一个延时任务,当任务到期的时候,再去zset中把数据取出来,返回给客户端使用. 就类似 DelayQueue 和 zset 的优化版本
- 推荐这种方式, 可以解决zset中的并发重复问题,实现方式也比较简单,稳定性、性能都比较高
4. 消息队列
(1). RabbitMQ的死信队列
- 优点: 消息机制靠谱, 高可用 , 可处理大量任务
- 缺点: 需要额外搭建环境和运维成本, 较为复杂
(2). RabbitMQ插件
- 优点: rabbitmq_delayed_message_exchange, 官方出品 , 值得信赖, 优化版本, 不存在消息阻塞
- 缺点: 和上面一样
总结
所以说具体用哪一种方案, 还是要结合自己项目情况来使用, 并没有说谁是最好的, 贴一个大佬(Hollis)的建议总结 :
[ 实现的复杂度上(包含用到的框架的依赖及部署):
Redisson > RabbitMQ插件 > RabbitMQ死信队列 > RocketMQ延迟消息 ≈ Redis的zset > Redis过期监听 > 定时任务 > JDK自带的DelayQueue
方案的完整性:
Redisson ≈ RabbitMQ插件 > Redis的zset ≈ RocketMQ延迟消息 ≈ RabbitMQ死信队列 > Redis过期监听 > 定时任务 > JDK自带的DelayQueue > 被动关闭
不同的场景中也适合不同的方案:
- 单体应用,业务量不大:Netty的时间轮、JDK自带的DelayQueue、定时任务
- 分布式应用,业务量不大:Redis过期监听、RabbitMQ死信队列、Redis的zset、定时任务
- 分布式应用,业务量大、并发高:Redisson、RabbitMQ插件、kafka时间轮、RocketMQ延迟消息
总体考虑的话,考虑到成本,方案完整性、以及方案的复杂度,还有用到的第三方框架的流行度来说,比较建议优先考虑Redisson+Redis、RabbitMQ插件、Redis的zset、RocketMQ延迟消息等方案 ]