消息队列实现延迟任务方法详解(详解订单30分钟未支付自动取消的多种方案)

实现”订单 30 分钟未支付自动取消”,本质是把一条消息延后消费。最稳走 MQ 延迟消息,轻量用 Redis ZSet,最省事是定时任务扫库。下面给出各方案代码与取舍。

一、四种方案横向对比

方案 实时性 可靠性 复杂度 适用规模
定时任务扫库 低(受扫描间隔) 中(需幂等) 低 小系统、低频
Redis ZSet 延迟队列 中(秒级) 中(依赖 Redis 高可用) 中 中等规模
RabbitMQ 死信队列 高 高(持久化+确认) 高 中大型生产
RocketMQ 延迟消息 高(固定级别) 高 中 已用 RocketMQ 的团队

订单超时关单属于典型延迟任务,同类还有:下单 60 秒后发通知、退款状态定时核对、新店 N 天未上架发激活短信。

二、方案 A:定时任务扫库

用 @Scheduled 或 Quartz 周期性查”待支付且创建超 30 分钟”的订单批量置为已取消,实现简单、不依赖中间件,但扫描间隔带来时间误差。落地按四步推进:

  1. 建查询方法按状态与截止时间取超时订单;
  2. 用 @Scheduled 设固定间隔(如 60 秒)触发;
  3. 批量更新为已取消并释放库存;
  4. 集群下加分布式锁,避免多节点重复取消。
@Scheduled(fixedDelay = 60_000)
public void cancelExpiredOrders {
    Date deadline = new Date(System.currentTimeMillis - 30 * 60_000);
    List<Order> list = orderMapper.selectUnpaidBefore(deadline);
    for (Order o : list) {
        orderMapper.updateStatus(o.getId, OrderStatus.CANCELED);
        inventoryService.release(o.getId);
    }
}

三、方案 B:Redis ZSet 延迟队列

把订单号作为 member、超时时间戳(创建时间 + 30 分钟)作为 score 写入 ZSet;后台线程每秒用 ZRANGEBYSCORE 捞 score 小于当前时间的元素并删除。

3.1 入队与消费

import redis, time
r = redis.Redis

def enqueue(order_id, delay_sec=1800):
    r.zadd("delay:order", {order_id: time.time + delay_sec})

def consume_loop:
    while True:
        due = r.zrangebyscore("delay:order", 0, time.time, start=0, num=100)
        for oid in due:
            if r.zrem("delay:order", oid) and query_status(oid.decode) == "UNPAID":
                cancel_order(oid.decode)
        time.sleep(1)

多消费者可能取到同一订单号,用 ZREM 的返回值判定谁抢到,保证幂等。

四、方案 C:RabbitMQ 死信队列

消息带 TTL 发往没有消费者的缓冲队列,30 分钟过期后变死信,进死信队列由消费者取消订单。

4.1 声明与发送

@Bean
public Queue bufferQueue {
    return QueueBuilder.durable("buffer.q")
        .ttl(30 * 60 * 1000)
        .deadLetterExchange("dlx.exchange")
        .deadLetterRoutingKey("dlx.key")
        .build;
}

rabbitTemplate.convertAndSend("buffer.exchange", "buffer.key", orderId);

死信消费者收到后查订单状态,未支付才取消并释放库存。注意:RabbitMQ 队列级 TTL 下,第一条消息没过期,后面的消息即便先到期也不提前投递,造成队头阻塞。3.5.8 之后建议装 rabbitmq_delayed_message_exchange 插件,按单条消息延时。

五、方案 D:RocketMQ 延迟消息

RocketMQ 原生支持延迟消息,但只有 18 个固定级别,其中正好含 30m 这一级。

Message msg = new Message("order_topic",
    ("cancel:" + orderId).getBytes);
msg.setDelayTimeLevel(16);
producer.send(msg);

消费者收到后检查订单状态,未支付则取消。任意时间精度无法用固定级别表达。

六、选型与可靠性注意

MQ 方案带来解耦与高可用,但也引入消息丢失、重复消费、顺序性问题,必须做消费幂等和失败重试。小系统用定时任务扫库最省心;要较好时效性又不想引入重中间件,选 Redis ZSet;已在用 RabbitMQ/RocketMQ 且要求可靠投递,直接用各自的延迟能力。

常见问题(FAQ)

Q1:定时任务扫库能用在生产吗?

能,但订单量大时压库且有时延,集群需做幂等防重复取消。

Q2:RabbitMQ 死信队头阻塞怎么破?

装 delayed message 插件,按单条消息设 x-delay,避免队头阻塞。

Q3:RocketMQ 能任意延时吗?

不能,仅 18 个固定级别,但含 30m,刚好覆盖订单超时场景。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
参数猎人的头像参数猎人认证作者

相关推荐

返回顶部