【关单机制系列 1】从关单机制的“Hello World”方案开始说起

163 阅读4分钟

最近一段时间参与的一个电商平台项目中,关于支付模块设计到一个功能:对于用户下单但长时间未支付的订单,系统应自动将其关闭。这个看似简单的需求,背后其实有不少实现方案可选,比如利用数据库的定时任务、消息队列的延迟消息、Redis 的过期键等。每种方案都有各自的优缺点,比如延迟精度、系统开销、可扩展性等。面对这些选择,我们该如何权衡利弊,选择最适合当前业务场景的方案呢?

解决方案

这看到这个需求的时候,想到了一些常见的解决方案:

  • 定时任务
  • JDK 延迟队列 DelayQueue
  • Redis 过期监听
  • Redisson 分布式延迟队列
  • RabbitMQ 死信队列(DLX)
  • RocketMQ 延迟消息
  • RabbitMQ 延迟消息插件(x-delayed-message)

接下来我们就掰扯掰扯,这些方案都有哪些优缺点,怎么实现,先上个对比图,总体概览一下

序号实现方案实现复杂度延迟精度分布式支持消息可靠性优点缺点
1定时任务⭐️分钟级简单直观、无依赖精度低、易阻塞、不支持分布式
2JDK DelayQueue⭐️⭐️毫秒级轻量级、无需中间件内存级别,重启丢失,不支持分布式
3Redis 过期监听⭐️⭐️秒级⚠️(需注意集群)❌(丢事件可能)上手快、无额外中间件集群监听不可靠、事件可能丢失
4Redisson 延迟队列⭐️⭐️⭐️秒级⚠️API 简洁、支持分布式延迟控制精度一般、依赖 Redis
5RabbitMQ 死信队列⭐️⭐️⭐️毫秒级成熟方案、可靠性强配置复杂、理解成本高
6RocketMQ 延迟消息⭐️⭐️⭐️等级延迟延迟可靠、性能好延迟粒度不细、固定级别限制
7RabbitMQ 延迟消息插件⭐️⭐️⭐️⭐️毫秒级自定义精度高、灵活需安装插件、版本兼容性问题

定时任务:每晚巡逻一次的小保安

在订单未支付自动关闭的众多方案中,定时任务是最简单粗暴的一种方式。它就像一个每隔一段时间巡逻一圈的小保安,一旦发现超时未付款的订单,就一脚把它踢出系统。

虽然这个方案原始又粗糙,但简单直接,胜在易于上手。

🎯 实现思路

  1. 固定时间间隔(如每分钟)执行一次定时任务
  2. 查询超过 15 分钟未支付的订单
  3. 将其状态更新为“已关闭”
  4. 同时执行相关业务操作,如:库存回滚、发送通知、记录日志

示例代码(Spring Boot)

@Slf4j
@Component
public class OrderAutoCloseTask {

  @Autowired
  private OrderService orderService;

  /**
   * 每分钟扫描一次
   */
  @Scheduled(cron = "0 */1 * * * ?")
  public void autoCloseTimeoutOrders() {
    log.info("开始执行自动关单任务...");
    // 查询超时的订单
    List<Order> timeoutOrders = orderService.findTimeoutUnpaidOrders(15);
    for (Order order : timeoutOrders) {
      try {
        orderService.closeOrder(order.getId());
        log.info("订单 {} 已超时关闭", order.getId());
      } catch (Exception e) {
        log.error("关闭订单 {} 失败", order.getId(), e);
      }
    }
  }
}

查询逻辑示例

public List<Order> findTimeoutUnpaidOrders(int minutes) {
  LocalDateTime cutoff = LocalDateTime.now().minusMinutes(minutes);
  return orderMapper.selectList(new LambdaQueryWrapper<Order>()
      .eq(Order::getStatus, OrderStatus.UNPAID)
      .lt(Order::getCreateTime, cutoff));
}

这种实现超级简单,只要一段定时代码 + 一个数据库查询,不需要消息队列、缓存等中间件,适合需要快速实现的时候。

这里面最大的问题有两个,一个是时间可能不够精确,例如每分钟扫描一次,最差可能延迟将近 59 秒,不适合需要精确时间的场景。第二个问题就是数据库压力比较大,如果缩短时间就会频繁的访问数据,而且每次扫描可能涉及大量历史订单,随着订单体量增加压力指数增长。另外也会出现无效的操作,比如没有超时订单,也要每次都查一遍,存在资源浪费

优化建议

既然存在问题,那么是不是也能找到一些优化方案呢?虽然感觉这是在屎上雕花☠️

1、可以增加缓存中中间件,可将新订单信息缓存到 Redis,定时任务只处理缓存中存在的超时订单(半持久化思路) 这个就很想到接下来要讲的Redis 过期监听方案。

2、数据库优化,给为 statuscreate_time 字段加索引,加速查询

3、数据库优化,使用分页查询,避免一次查出过多订单,控制数据库压力


定时任务是关单机制的“Hello World”方案,小而美、稳中取巧,非常适合系统初期或开发阶段验证逻辑。但它终究只是“临时工”方案,不适合应对复杂的大规模订单处理场景。