如何实现订单到期自动关闭?
2024年7月12日大约 4 分钟场景题
如何实现订单到期自动关闭?
定时任务
定期检查数据库中的订单记录
对于已经创建但未在设定时间内完成支付的订单,系统自动将其状态更新为「已关闭」
可以使用 elastic-job
、xxl-job
等框架
定时任务这种方式简单有效,但是存在几个问题
- 时间精确性
定时任务一般基于固定的时间频率来执行的
那么,就会存在订单已经过期,但还没扫描到,那就导致订单比预期时间晚关闭,存在精度问题
如果定时任务设置的过于频繁,那它可能又会对服务器产生不小的影响 - 海量订单处理困难
定时任务不仅精度差,而且一次可能会筛选出来许多符合的数据
数据量一大,处理时间就会变长,导致任务超时 - 数据库压力大
短时间内需要处理大量的订单状态更新,可能会导致数据库高负荷甚至暂时的服务拥堵
RocketMQ 延迟消息
RocketMQ 天然支持延迟消息,如订单支付超时、发送定时通知等
在 RocketMQ 中, 可以很容易地实现消息的延迟投递
RocketMQ 并没有直接存储原始的延迟时间,而是提供了几个预定义的延迟级别,每个级别对应一个具体的时间长度
当你发送一个延迟消息时,你需要指定这个消息的延迟级别
消息被存储在 Broker 上后,并不会立即投递,而是等待特定的时间后才被投递到目标队列
比如,用户创建完订单后,希望在 15 分钟内,如果没有完成支付,就取消订单
那我们在用户创建完订单后,发个延迟 15 分钟的消息,那 15 分钟后,这个消息就会被消费者消费
但是,RocketMQ 4.x 版本并不支持任意的延迟时间,它只支持 18 个延迟级别,从 1s 到 2h
DelayQueue(Java)
DelayQueue 是一个无界阻塞队列,用于放置实现了 Delayed 接口的对象,其中的元素只能在其到期时才能从队列中取出
这种方案可以做到精确的时间控制,而且线性安全(毕竟都在一个 Java 进程中)
但是 DelayQueue 是无界的,使用不当可能导致内存溢出
而且不适合分布式系统中的应用,重启的时候,排队的任务可能会丢失
ZSet(Redis)
Redis 的有序集合也可以实现订单到期,自动关闭
- 订单到期时间作为分数(score)
将订单的到期时间戳作为 ZSet 中每个订单的分数,这样就可以通过分数范围查询到期订单 - 订单 ID 作为成员(member)
- 定期检查过期订单
例如,每分钟执行一个检查任务,查询 ZSet 中分数小于当前时间戳的订单 ID(即已过期的订单) - 关闭过期订单
对于过期订单,执行关闭订单的操作
这种方式提供高效的查询和操作,Redis 的 ZSet 提供了高效的有序集合数据结构,能够快速执行插入、删除、范围查询等操作,使得对订单到期时间的管理更加高效
借助 Redis 的持久化、高可用机制,也可以避免数据丢失
HashedWheelTimer(Netty)
Netty 中的 HashedWheelTimer 是一种基于时间轮算法的定时器,常用于处理定时任务和延时任务
- 创建 HashedWheelTimer 实例,用于执行定时任务
- 添加到期任务
当用户下单时,计算订单的过期时间,并将过期任务添加到 HashedWheelTimer 中 - 处理到期任务
当到期时间到达时,HashedWheelTimer 会执行相应的任务,这时就可以在任务中执行关闭订单的操作了
这种方式也较为简单,适合单机场景,但是需要引入额外的组件