如何关闭已超时的订单 ?

背景:

        我们逛淘宝的时候,遇到想要的商品,支付的时候,会有时间的限制,我们在时间限制里面完成支付,就会出订单号。但是如果逾期了,就会关闭超时订单。下面说一下思路以及伪代码。

一:定时器实现

思路:
  1. 存储机制:在订单创建时记录超时时间字段 expire_time
  2. 定时轮询:每5秒扫描数据库查询未支付订单
  3. 索引优化:通过复合索引加速查询
  4. 批量处理:分页查询避免单次处理数据量过大

伪代码:
CREATE TABLE orders (
  order_id BIGINT PRIMARY KEY,
  status TINYINT,       -- 0未支付 1已支付
  expire_time DATETIME, -- 超时时间
  created_time DATETIME,
  updated_time DATETIME,
  INDEX idx_expire_status (expire_time, status) -- 复合索引
);
// 伪代码 Spring Boot定时任务示例
@Scheduled(fixedDelay = 5000)
public void checkExpiredOrders() {
    int pageSize = 100;
    long lastId = 0;
    
    while(true) {
        // 使用游标分页查询
        List orders = orderRepository.findExpiredOrders(
            LocalDateTime.now().minusSeconds(5),
            lastId,
            pageSize
        );
        
        if(orders.isEmpty()) break;
        
        orders.forEach(order -> {
            // 分布式锁防止并发处理
            String lockKey = "order_lock:" + order.getId();
            if(redisLock.tryLock(lockKey, 10)) {
                try {
                    if(order.getStatus() == OrderStatus.UNPAID) {
                        // 事务内处理订单
                        processExpiredOrder(order);
                    }
                } finally {
                    redisLock.unlock(lockKey);
                }
            }
        });
        
        lastId = orders.get(orders.size()-1).getId();
    }
}

// JPA查询方法
@Query("SELECT o FROM Order o WHERE o.status = 0 AND o.expireTime < :current AND o.id > :lastId ORDER BY o.id ASC")
List findExpiredOrders(@Param("current") LocalDateTime current, 
                             @Param("lastId") Long lastId,
                             Pageable pageable);

优点

1,无需中间件依赖

2,数据一致性由数据库事务保证

3,代码实现简单

缺点

1,存在最多5秒延迟

2,高频查询对数据库有压力

二:优化被动关闭订单方案:按需处理与客户端协同

思路:

在传统的订单超时处理中,服务端通常需要主动轮询或依赖延迟消息触发关闭动作。而另一种思路是:将超时判断逻辑下沉到客户端,由客户端按需触发关闭请求: 

1,数据拉取阶段 当客户端请求订单列表时,服务端返回订单原始数据,包含订单创建时间超时时间阈值以及服务端当前时间戳

2,客户端动态计算 客户端根据服务端时间与订单创建时间,实时计算剩余支付时间。若检测到超时,则将订单标记为“已过期”样式,同时在用户无感知的情况下,异步发送关闭请求到服务端

3,基本上面思路已经很完善,假设还想再优化一下,就是服务端接收到关闭的请求,二次校验,校验当前订单的状态,实际超时时间。

优点:

1,无需高频轮询或维护延迟队列,尤其适合低频访问,服务端仅在实际需要时处理关闭逻辑。减少引入中间件的成本以及时间投入。

缺点:

1,脏数据有滞留现象,若用户长期不访问客户端,超时订单可能未被及时关闭,影响准确性

2,对于客户端的依赖性很强。

三:延迟队列

思路:

1,消息生产阶段

1.1订单创建时同步写入数据库

1.2发送携带订单ID的延迟消息到RocketMQ

1.3根据业务超时时间选择最接近的延迟等级

 2.消息消费阶段

2.1消费者监听指定Topic的消息

2.2收到消息后查询订单最新状态

2.3校验订单是否仍处于未支付状态

2.4执行关单业务逻辑

3 补偿机制

3.1独立定时任务扫描未支付订单

3.2处理RocketMQ可能丢失的消息

伪代码:

生产者:

// 订单创建时发送延迟消息(伪代码)
public class OrderProducer {
    private RocketMQTemplate rocketMQTemplate;

    public void createOrder(Order order) {
        // 1. 写入数据库
        orderDao.insert(order);
        
        // 2. 发送延迟消息
        Message message = MessageBuilder.withPayload(order.getOrderId())
                .setHeader(RocketMQHeaders.KEYS, order.getOrderId())
                .build();
        int delayLevel = 16;  // 对应30分钟
        rocketMQTemplate.syncSend("ORDER_TIMEOUT_TOPIC", message, 3000, delayLevel);
        
      
    }
}

消费者:

@RocketMQMessageListener(
    topic = "ORDER_TIMEOUT_TOPIC",
    consumerGroup = "ORDER_TIMEOUT_GROUP"
)
public class OrderTimeoutConsumer implements RocketMQListener {
    @Override
    public void onMessage(MessageExt message) {
        String orderId = new String(message.getBody());
        
        Order order = orderDao.getById(orderId);
        if (order == null) return;
        
        // 核心校验逻辑
        if (order.getStatus() == OrderStatus.UNPAID) {
            // 执行关单操作
            orderService.closeOrder(order);
            log.info("关闭超时订单: {}", orderId);
        } else {
  
            log.warn("收到已处理订单: {}", orderId);
        }
    }
}


这里兜底一下:

虽然rocketmq有持久策略,这里使用了第一种方式兜底,帮助理解。

@Scheduled(cron = "0 0/5 * * * ?")
public void checkOrderTimeout() {
    // 扫描数据库中状态为未支付且超时的订单
    List expiredOrders = orderDao.scanExpiredOrders();
    
    expiredOrders.forEach(order -> {
        if (order.getStatus() == OrderStatus.UNPAID) {
            orderService.closeOrder(order);
        }
    });
}

优点:

1,天然分布式特性,适合高并发场景

2,借助MQ实现解耦,降低业务侵入性

缺点:

1,无法取消已发送的延迟消息,导致无效消息处理

2,消息堆积时可能产生处理延迟

3,强依赖MQ中间件稳定性

四: 超时中心(TOC)

思路:
  1. 任务提交

    1.1.业务系统向超时中心注册任务  1.2示例:订单系统提交任务:“订单ID=123,30分钟后触发,回调地址为/order/xxxx”。
  2. 任务存储

    2.1所有任务按触发时间排序存储。 2.2常用存储方式:数据库表、Redis Sorted Set、时间轮算法。
  3. 任务调度

    3.1定时扫描“即将触发”的任务.  3.2触发时,将任务发送到延迟队列或直接调用业务回调接口。
  4. 任务处理

    4.1 触发后,超时中心调用业务系统的接口通知:“订单ID=123已超时,请处理”。 4.2业务系统自行实现具体逻辑(如关闭订单)。

优点

1,针对超时任务调度场景专项设计,通过分布式架构提高吞吐。

2,基于时间轮、延迟队列等机制,可支撑千万级任务调度。

3,专人维护,质量好。

缺点

1,技术成本和维护成本高。

2,实现复杂度比较高,会涉及分布式调度,MQ,高存储(redis)等,面临分布式锁,任务分片,网络抖动,节点宕机一系列问题,并且运维复杂度增加。

你可能感兴趣的:(数据库)