跳到主要内容

订单超时处理

几种常见的处理办法

在软件开发中,经常会遇到延时任务的需求,如订单生成 15 分钟未支付自动取消、60 秒后给用户发送短信通知等。延时任务和定时任务有所不同,定时任务有明确触发时间和固定执行周期,而延时任务在事件触发后一段时间内执行,无固定周期,通常针对单个任务。

对于判断订单是否超时,有以下几种常见方案及优缺点:

方案一:定时查询数据库

通过线程定时扫描数据库,根据订单时间判断是否超时并执行相应操作。在单体应用中,可使用 Spring TaskQuartz 等单机任务调度工具;在集群环境下,可使用支持分布式任务调度的工具,如 XXL-JobElastic Job 等。

优点:简单且支持集群,是小项目的首选。 缺点:对服务器内存消耗大,存在延迟,对数据库性能影响大,部分工具部署和配置有成本。

方案二:使用 JDK 延迟队列

JDK 中有实现无界阻塞队列的 DelayQueue ,可在延迟期满时获取元素,方便实现订单超时检测。put 方法获取元素为空时返回空,take 方法未获取到元素会一直等待。

优点:效率高,任务触发延迟低。 缺点:服务器重启数据丢失,集群拓展麻烦,内存有限制,代码维护复杂度高。

方案三:时间轮算法

模拟现实时钟,有一轮的 TIK 数、每转一次的持续时间和单位时间等关键参数。

优点:效率较高,代码复杂度较低。 缺点:数据存储在内存,存在服务器重启数据丢失和集群拓展麻烦的问题,也有内存限制。

方案四:Redis 缓存

  • 定时扫描有序集合:将订单号作为 key ,时间戳作为 value ,按时间戳排序,判断是否超时。
  • 利用过期监听机制:设置订单号 key 的过期时间,监听过期执行相应操作。
  • 客户端缓存监听:本地缓存与 Redis 缓存关联,通过发布订阅机制通知变更。

优点:对数据库压力小,具备主动推送功能,方便集群拓展,时间准确度高,重启可读取数据处理,可指定实例负责取消减少消息数量。 缺点:需要额外维护 Redis ,长连接重连需补偿,Redis 集群变化时需处理实例预分配。

方案五:消息队列方案

RocketMQ 为例,其支持延迟消息,可指定时间消费,也可利用死信队列实现类似功能。

优点:消息及时投递,集群支持好,代码量小。 缺点:依赖重量级 MQ 组件,需熟悉其机制,处理幂等性等问题。

综上所述,不同的方案各有优缺点,在实际开发中,应根据项目规模、技术架构和需求特点选择合适的方案来实现延时任务。

Redisson解决订单超时

我们可以使用Redis的Zset结构,过期时间作为分数,订单号作为key

创建订单,保存到Redis的Zset中

// 订单信息保存到redis 采用zest数据结构
String orderNo = order.getOrderNo();
String key = RedisKey.getKey(RedisKey.ORDER_LIST);
long score = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(PayConstant.ORDER_EXPIRE_TIME);
RScoredSortedSet<String> scoredSortedSet = redissonClient.getScoredSortedSet(key);
scoredSortedSet.add(score, orderNo);

再开启一个定时任务,每秒去扫描有没有过期的订单:

// 1s执行一次 移除过期订单
@Scheduled(fixedRate = 1000)
public void removeExpiredOrder() {
String key = RedisKey.getKey(RedisKey.ORDER_LIST);
RScoredSortedSet<String> orderSet = redissonClient.getScoredSortedSet(key);
// 获取”当前时间 > score”的延时任务 获取0 到当前时间的所有订单 true表示包含当前时间 [0,当前时间]
Collection<String> values = orderSet.valueRange(0, true, System.currentTimeMillis(), true);
// 转为list
List<String> expiredOrderList = (List<String>) values;
expiredOrderList.forEach(orderNo -> {
log.warn("当前时间:{},订单号:{} ,已过期", LocalDateTime.now(), orderNo);
});
// 更新过期订单状态
orderService.updateExpiredOrders(expiredOrderList);
// 删除Redis过期订单
orderSet.removeAll(expiredOrderList);
}