如何取消超时未支付的订单_

最近很多同学面试都被问到了这个问题,但他们给出的技术方案并不能让面试官满意,我在本文中给出一个无法辩驳的最优解。

目前主流的技术方案有如下四种:基于RocketMQ的延时队列方案、基于Redis ZSet的延时队列方案、基于Redis的过期监听方案,以及基于XXL-JOB的定时轮询方案。

1、基于RocketMQ的延时队列方案

RocketMQ是直接支持延时队列的,在其4.X版本:支持18个固定延迟级别

(1s/5s/10s/30s/1m/…/30m/1h/2h),需根据业务需求选择合适的级别。

生产者端代码如下:

Message msg = new Message("ORDER_DELAY_TOPIC", 
    "订单已创建,等待支付".getBytes());
// 设置延迟级别(对应30分钟:RocketMQ预设level=16)
msg.setDelayTimeLevel(16); 
SendResult sendResult = producer.send(msg);

消费者端代码如下:

consumer.subscribe("ORDER_DELAY_TOPIC", "*", new MessageListener() {
    public Action consume(Message message, ConsumeContext context) {
        String orderId = parseOrderId(message);
        if (checkOrderUnpaid(orderId)) { // 校验订单未支付
            cancelOrder(orderId);        // 执行取消操作
        }
        return Action.CommitMessage;
    }
});

而到了RocketMQ 5.X版本,就支持任意时刻的延迟消息了,可通过客户端API直接指定延迟时间,灵活性更高。

2、基于Redis ZSet的延时队列方案

Redis Zset(SortedSet),是Set的可排序版,是通过增加一个排序属性score来实现的,适用于排行榜和时间线之类的业务场景。

如下图所示,这里的score属性对应的是销售额:

我们可以通过Redis Zset来实现延时队列的功能,具体思路是:在生产者端向Zset中添加元素,并设置score值为元素过期的时间。

在消费端对Zset进行轮询,将元素的score值与当前时间进行比对,将小于当前时间的过期key进行移除。

生产者端代码如下:

// 用户下单后,设置30分钟超时
long cancelTime = System.currentTimeMillis() + 30 * 60 * 1000;
redisTemplate.opsForZSet().add("order:delay:queue", orderId, cancelTime);

消费者端代码如下:

// 获取当前时间前到期的订单ID集合(0表示从最小分数开始)
    Set<String> expiredOrders = redisTemplate.opsForZSet().rangeByScore(
        "order:delay:queue", 0, now);

    if (expiredOrders != null && !expiredOrders.isEmpty()) {
        // 原子操作:删除已处理订单并返回实际删除数量
        Long removedCount = redisTemplate.opsForZSet().remove(
            "order:delay:queue", expiredOrders.toArray(new String[0]));
        if (removedCount > 0) {
            // 批量取消订单(幂等处理)
            batchCancelOrders(expiredOrders);
        }
    }
    Thread.sleep(1000);
}
🚀 立即加入私密社群!
🔥 免费领取《DeepSeek使用指南》

🟢 绿泡泡直达:tutou123.com
🔑 暗号:架构升级(优先审核)

**3、基于Redis的过期监听方案**

基于Redis的过期监听实现取消订单的方案,核心是利用Redis的**键过期事件**,在订单的取消时间到达时自动触发取消逻辑。

修改<font style="color:rgb(51, 51, 51);">redis.conf文件:</font>

notify-keyspace-events Ex  # 启用键过期事件

订单创建代码:

// Spring Boot 示例(RedisTemplate)
public void createOrder(Order order) {
    // 写入数据库(状态为未支付)
    orderRepository.save(order);

    // 设置 Redis 键,30分钟过期
    String key = "order:unpaid:" + order.getId();
    redisTemplate.opsForValue().set(key, order.getId());
    redisTemplate.expire(key, 30, TimeUnit.MINUTES);
}

订单过期事件监听:

@Component
public class RedisKeyExpirationListener {
    @Autowired
    private OrderService orderService;

    @EventListener
    public void handleKeyExpiration(RedisKeyExpiredEvent<?> event) {
        String orderId = new String(event.getId());
        if (orderId.startsWith("order:unpaid:")) {
            orderService.cancelOrder(orderId.replace("order:unpaid:", ""));
        }
    }
}

**4、基于XXL-JOB的定时轮询方案**

基于XXL-JOB的定时轮询实现取消订单的方案,是一种通过分布式任务调度平台定时扫描数据库,取消超时未支付订单的方式。

XXL-JOB调度中心:作为核心控制平台,负责任务的配置、触发和状态记录,支持集群化部署,保障任务调度的高可用性。

业务系统执行器:集成XXL-JOB客户端,接收调度中心指令,执行订单取消逻辑,包括订单状态更新、库存释放等操作。

执行器代码如下:

@XxlJob("cancelExpiredOrdersJob")
public void cancelExpiredOrders() {
    // 查询超时未支付订单
    List<Order> expiredOrders = orderService.findExpiredOrders();
    for (Order order : expiredOrders) {
        try {
            // 执行取消订单操作,如更新状态、释放库存等
            orderService.cancelOrder(order.getId());
            log.info("成功取消订单: {}", order.getId());
        } catch (Exception e) {
            log.error("取消订单失败,订单ID: {}", order.getId(), e);
        }
    }
}

**有何问题?**

我们在本文开头中提到过,最近很多同学面试都被问到了这个问题,但他们给出的技术方案并不能让面试官满意,当然也包括上述四种技术方案。

因为面试官通常会问如下两个问题:

1、你通过RocketMQ/Redis/XXL-JOB来实现的取消超时未支付的订单,那如果RocketMQ/Redis/XXL-JOB挂了怎么办?

2、RocketMQ/Redis/XXL-JOB可能会存在处理延迟的问题,不能在30分钟精准地取消过期未支付订单,那你要如何解决?

而这两个问题通常会把面试经验不足的同学难住。

对于第一个问题,确实在RocketMQ和Redis挂了且没做Plan B的情况下,那只能寄希望于RocketMQ和Redis集群快点儿恢复了。

这还不算完,还需要对故障时间内的过期未关单数据进行手动处理。

但XXL-JOB就不一样了,就算它的调度中心集群宕机了,我们仍然可以直接向执行器发送请求即可触发任务,或者通过一个操作系统任务定时发送请求。

格式如下:

POST http://执行器IP:端口/run
Content-Type: application/json

{
  "jobId": 任务ID,
  "executorHandler": "任务处理器名称",
  "executorParams": "任务参数",
  "logId": 日志ID(可随机生成),
  "broadcastIndex": 0,
  "logDateTime": 当前时间戳
}

对于第二个问题,其实根本就TMD不是问题。

我们试想一下,如果用户在第30分01秒对未支付订单进行支付了,那到底是给公司带来损失了,还是带来收入了?

显然是后者!

所以做技术不能死做技术,应该去结合业务场景去制定技术方案,取消超时未支付的订单本质上是为了让订单进入终态,以解决财务结算的问题而已。

**终极方案**

当然,从技术方案上也是有解的,我们想想在Redis底层是如何清理过期Key的?

Redis用的是定期清理和惰性清理相结合的方式。

定期清理

Redis会将每个设置了过期时间的key放入到一个独立的字典中,以后会定期遍历这个字典来删除到期的key。

Redis默认会每秒进行十次过期扫描,过期扫描不会遍历过期字典中所有的key,而是采用了一种简单的贪心策略。

(1)从过期字典中随机取出20个key。

(2)清理这20个key中已经过期的key。

(3)如果过期的key比率超过25%,那就再重复执行一次步骤(1)(2)。

同时,Redis为保证定期扫描不会出现“贪心”过度,从而导致线程卡死现象,在算法上还增加了扫描时间的上限,默认不会超过25ms。

惰性删除

在客户端访问某个key的时候,Redis对该key的过期时间进行检查,如果过期了就立即删除。

回到原题上,如果真的需要精准地取消超时未支付的订单,那我们参考Redis的惰性删除策略,在订单支付的时候进行二次校验就好了。

所以,解决取消超时未支付的订单的终极方案,就是基于XXL-JOB的定时轮询 + 订单支付时二次校验兜底。

这样可以在功能可用性、开发复杂性和处理时延性上达到最优解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值