超时订单处理方案实战指南【完整版】

文章目录

定时任务扫描方案

方案原理

通过定时任务周期性扫描数据库,查询过期订单并批量处理。

核心流程

plain 复制代码
定时任务触发 → 查询过期订单 → 遍历处理 → 更新状态 → 后续操作
     ↓              ↓             ↓          ↓          ↓
  每分钟1次      LIMIT 1000    并发处理    乐观锁    恢复库存
                                                    释放优惠券
                                                    发送通知

关键技术点

java 复制代码
// 1. 过期时间字段+索引
ALTER TABLE t_order ADD COLUMN expire_time DATETIME;
CREATE INDEX idx_status_expire ON t_order(status, expire_time);

// 2. 分布式锁保证单实例执行
RLock lock = redisson.getLock("schedule:order-expire");
if (lock.tryLock(0, 50, TimeUnit.SECONDS)) {
    // 处理逻辑
}

// 3. 乐观锁防止并发冲突
UPDATE t_order SET status = 'CANCELLED', version = version + 1
WHERE order_id = ? AND status = 'CREATED' AND version = ?;

// 4. 限流控制处理速率
RateLimiter rateLimiter = RateLimiter.create(50.0);  // 每秒50单

优缺点分析

维度 评价 说明
实现难度 ⭐⭐ Spring @Scheduled或XXL-Job即可
时效性 ⭐⭐⭐ 取决于扫描频率,通常1-2分钟延迟
准确性 ⭐⭐⭐⭐⭐ 基于数据库,数据不会丢失
性能 ⭐⭐⭐ 频繁扫描数据库,有一定压力
扩展性 ⭐⭐⭐ 可通过分片提升,但有上限

Redis过期监听方案

方案原理

订单创建时在Redis中设置key,利用Redis的键过期事件(Keyspace Notifications)触发订单取消。

核心流程

plain 复制代码
创建订单 → Redis SET key → key过期 → 监听到过期事件 → 取消订单
   ↓           ↓              ↓            ↓              ↓
 order_1   expire:30min    自动删除   KeyExpired    查DB+处理

实现步骤

1. Redis配置

properties 复制代码
# redis.conf
notify-keyspace-events Ex
# E: Keyspace events
# x: 过期事件

2. 订单创建时写入Redis

java 复制代码
@Service
public class OrderService {
    
    public Order createOrder(OrderRequest request) {
        // 1. 创建订单
        Order order = saveOrder(request);
        
        // 2. 写入Redis,设置过期时间
        String key = "order:expire:" + order.getOrderNo();
        redisTemplate.opsForValue().set(key, order.getId().toString(), 30, TimeUnit.MINUTES);
        
        return order;
    }
}

3. 监听Redis过期事件

java 复制代码
@Component
public class RedisKeyExpiredListener extends KeyExpirationEventMessageListener {
    
    @Autowired
    private OrderCancelService orderCancelService;
    
    public RedisKeyExpiredListener(RedisMessageListenerContainer container) {
        super(container);
    }
    
    @Override
    public void onMessage(Message message, byte[] pattern) {
        String expiredKey = message.toString();
        
        // 解析订单号
        if (expiredKey.startsWith("order:expire:")) {
            String orderNo = expiredKey.replace("order:expire:", "");
            
            // 异步取消订单
            orderCancelService.cancelOrderAsync(orderNo);
        }
    }
}

常见问题

问题1: Redis过期事件不可靠

plain 复制代码
风险点:
1. Redis主从复制延迟,从库可能收不到过期事件
2. Redis宕机,过期事件丢失
3. 过期删除策略是懒惰删除+定期删除,不保证立即触发
4. 高并发下,过期事件可能延迟

解决方案:
- Redis过期监听作为触发机制
- 定时任务扫描作为兜底方案
- 双重保障,确保订单不漏处理

问题2: 消息消费幂等性

java 复制代码
// 同一个过期事件可能被多次消费
public void onMessage(Message message, byte[] pattern) {
    String expiredKey = message.toString();
    
    // 使用分布式锁保证只处理一次
    RLock lock = redisson.getLock("lock:" + expiredKey);
    if (lock.tryLock()) {
        try {
            processExpiredOrder(expiredKey);
        } finally {
            lock.unlock();
        }
    }
}

优缺点分析

维度 评价 说明
实现难度 ⭐⭐⭐ 需要理解Redis发布订阅机制
时效性 ⭐⭐⭐⭐ 秒级触发,但不保证精确
可靠性 ⭐⭐ Redis宕机会丢失事件
性能 ⭐⭐⭐⭐ 无需扫描数据库,性能好
扩展性 ⭐⭐⭐ 受Redis性能限制

适用场景:

  • 对时效性要求较高(秒级)
  • 订单量中等(日订单量<50万)
  • 需要结合定时任务兜底

Redis Sorted Set延迟队列

方案原理

利用Redis Sorted Set的score特性,将过期时间作为score,通过轮询获取到期任务。

核心流程

plain 复制代码
创建订单 → ZADD delay:order → 轮询ZRANGEBYSCORE → 获取到期订单 → 处理
   ↓            ↓                    ↓                 ↓           ↓
 order_1    score=过期时间戳    每秒查询一次        ZREM移除    取消订单

数据结构:

plain 复制代码
Redis Sorted Set: delay:order:cancel

┌──────────────┬────────────────────┐
│   Member     │      Score         │
├──────────────┼────────────────────┤
│  ORD001:1    │  1735084800000     │  ← 过期时间戳
│  ORD002:2    │  1735084860000     │
│  ORD003:3    │  1735084920000     │
└──────────────┴────────────────────┘

score = 订单创建时间 + 30分钟

完整实现

1. 订单创建时加入延迟队列

java 复制代码
@Service
public class DelayQueueService {
    
    private static final String DELAY_QUEUE_KEY = "delay:order:cancel";
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    public void addDelayTask(String orderNo, Long orderId, int delayMinutes) {
        // 计算过期时间戳
        long expireTime = System.currentTimeMillis() + delayMinutes * 60 * 1000L;
        
        // 存储格式: orderNo:orderId
        String member = orderNo + ":" + orderId;
        
        // 加入Sorted Set
        redisTemplate.opsForZSet().add(DELAY_QUEUE_KEY, member, expireTime);
    }
    
    public void removeDelayTask(String orderNo, Long orderId) {
        String member = orderNo + ":" + orderId;
        redisTemplate.opsForZSet().remove(DELAY_QUEUE_KEY, member);
    }
}

2. 轮询消费延迟队列

java 复制代码
@Component
public class DelayQueueConsumer {
    
    private static final String DELAY_QUEUE_KEY = "delay:order:cancel";
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Autowired
    private OrderCancelService orderCancelService;
    
    @Scheduled(fixedDelay = 1000)  // 每秒执行一次
    public void consumeDelayQueue() {
        long now = System.currentTimeMillis();
        
        // 查询已到期的任务
        Set<String> tasks = redisTemplate.opsForZSet()
            .rangeByScore(DELAY_QUEUE_KEY, 0, now, 0, 100);
        
        if (tasks == null || tasks.isEmpty()) {
            return;
        }
        
        for (String task : tasks) {
            // 解析订单信息
            String[] parts = task.split(":");
            String orderNo = parts[0];
            Long orderId = Long.parseLong(parts[1]);
            
            // 尝试删除(防止重复消费)
            Long removed = redisTemplate.opsForZSet().remove(DELAY_QUEUE_KEY, task);
            
            if (removed != null && removed > 0) {
                // 异步处理订单取消
                orderCancelService.cancelOrderAsync(orderId);
            }
        }
    }
}

3. 订单支付后移除延迟任务

java 复制代码
@Service
public class PaymentService {
    
    @Autowired
    private DelayQueueService delayQueueService;
    
    @Transactional
    public void payOrder(Long orderId) {
        // 1. 更新订单状态为已支付
        Order order = orderRepository.findById(orderId).orElseThrow();
        order.setStatus("PAID");
        orderRepository.save(order);
        
        // 2. 从延迟队列中移除
        delayQueueService.removeDelayTask(order.getOrderNo(), orderId);
    }
}

4.Lua脚本优化(原子性保障)

lua 复制代码
-- 原子性获取并删除到期任务
local key = KEYS[1]
local now = ARGV[1]
local limit = ARGV[2]

-- 获取到期任务
local tasks = redis.call('ZRANGEBYSCORE', key, 0, now, 'LIMIT', 0, limit)

if #tasks > 0 then
    -- 删除已获取的任务
    redis.call('ZREM', key, unpack(tasks))
end

return tasks
java 复制代码
public List<String> popExpiredTasks(int limit) {
    String script = 
        "local tasks = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1], 'LIMIT', 0, ARGV[2])" +
        "if #tasks > 0 then redis.call('ZREM', KEYS[1], unpack(tasks)) end" +
        "return tasks";
    
    List<String> result = redisTemplate.execute(
        new DefaultRedisScript<>(script, List.class),
        Collections.singletonList(DELAY_QUEUE_KEY),
        String.valueOf(System.currentTimeMillis()),
        String.valueOf(limit)
    );
    
    return result;
}

优缺点分析

维度 评价 说明
实现难度 ⭐⭐⭐ 需要理解Sorted Set和Lua脚本
时效性 ⭐⭐⭐⭐⭐ 秒级精确触发
可靠性 ⭐⭐⭐⭐ Redis持久化可保证数据不丢失
性能 ⭐⭐⭐⭐⭐ Redis操作高效,支持高并发
扩展性 ⭐⭐⭐⭐ 可多实例消费,水平扩展

适用场景:

  • 对时效性要求高(秒级)
  • 中大规模订单量
  • 已有Redis基础设施
  • 强烈推荐

RabbitMQ延迟队列

方案原理

利用RabbitMQ的死信队列(DLX)+ TTL机制实现延迟消息。

核心架构

plain 复制代码
┌──────────────┐         ┌──────────────────┐         ┌──────────────┐
│  订单服务     │ ──发送─→│   延迟队列        │ ──过期─→│   死信队列    │
│              │  消息   │  (TTL=30min)     │   后   │              │
└──────────────┘         │  无消费者         │         │   取消订单    │
                         └──────────────────┘         └──────────────┘
                                                              ↑
                                                              │
                                                        ┌──────────────┐
                                                        │  消费者监听   │
                                                        └──────────────┘

实现步骤

1. RabbitMQ配置

java 复制代码
@Configuration
public class RabbitMQConfig {
    
    // 延迟队列
    public static final String DELAY_QUEUE = "queue.order.delay";
    // 死信交换机
    public static final String DLX_EXCHANGE = "exchange.order.dlx";
    // 死信队列
    public static final String DLX_QUEUE = "queue.order.cancel";
    // 死信路由键
    public static final String DLX_ROUTING_KEY = "order.cancel";
    
    @Bean
    public Queue delayQueue() {
        return QueueBuilder.durable(DELAY_QUEUE)
            .withArgument("x-dead-letter-exchange", DLX_EXCHANGE)
            .withArgument("x-dead-letter-routing-key", DLX_ROUTING_KEY)
            .build();
    }
    
    @Bean
    public DirectExchange dlxExchange() {
        return new DirectExchange(DLX_EXCHANGE);
    }
    
    @Bean
    public Queue dlxQueue() {
        return new Queue(DLX_QUEUE);
    }
    
    @Bean
    public Binding dlxBinding() {
        return BindingBuilder.bind(dlxQueue())
            .to(dlxExchange())
            .with(DLX_ROUTING_KEY);
    }
}

2. 发送延迟消息

java 复制代码
@Service
public class OrderService {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    public Order createOrder(OrderRequest request) {
        // 1. 创建订单
        Order order = saveOrder(request);
        
        // 2. 发送延迟消息
        OrderDelayMessage message = new OrderDelayMessage();
        message.setOrderId(order.getId());
        message.setOrderNo(order.getOrderNo());
        
        rabbitTemplate.convertAndSend(
            RabbitMQConfig.DELAY_QUEUE,
            message,
            msg -> {
                // 设置TTL = 30分钟
                msg.getMessageProperties().setExpiration("1800000");
                return msg;
            }
        );
        
        return order;
    }
}

3. 消费死信队列

java 复制代码
@Component
public class OrderCancelConsumer {
    
    @Autowired
    private OrderCancelService orderCancelService;
    
    @RabbitListener(queues = RabbitMQConfig.DLX_QUEUE)
    public void handleExpiredOrder(OrderDelayMessage message) {
        log.info("收到过期订单消息: orderId={}", message.getOrderId());
        
        // 取消订单
        orderCancelService.cancelOrder(message.getOrderId());
    }
}

4. 订单支付后取消延迟消息(无法实现)

plain 复制代码
注意: RabbitMQ的TTL消息一旦发送,无法取消!

解决方案:
1. 消费时检查订单状态,如果已支付则忽略
2. 使用RabbitMQ延迟插件(rabbitmq_delayed_message_exchange)
  1. RabbitMQ延迟插件方案(推荐)
java 复制代码
@Configuration
public class RabbitMQDelayConfig {
    
    public static final String DELAY_EXCHANGE = "exchange.order.delay";
    public static final String DELAY_QUEUE = "queue.order.cancel";
    
    @Bean
    public CustomExchange delayExchange() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");
        
        return new CustomExchange(
            DELAY_EXCHANGE,
            "x-delayed-message",
            true,
            false,
            args
        );
    }
    
    @Bean
    public Queue delayQueue() {
        return new Queue(DELAY_QUEUE);
    }
    
    @Bean
    public Binding delayBinding() {
        return BindingBuilder.bind(delayQueue())
            .to(delayExchange())
            .with("order.cancel")
            .noargs();
    }
}

// 发送延迟消息
public void sendDelayMessage(OrderDelayMessage message, int delaySeconds) {
    rabbitTemplate.convertAndSend(
        DELAY_EXCHANGE,
        "order.cancel",
        message,
        msg -> {
            msg.getMessageProperties().setHeader("x-delay", delaySeconds * 1000);
            return msg;
        }
    );
}

优缺点分析

维度 评价 说明
实现难度 ⭐⭐⭐ 需要理解RabbitMQ机制
时效性 ⭐⭐⭐⭐⭐ 精确到秒级
可靠性 ⭐⭐⭐⭐⭐ 消息持久化,高可靠
性能 ⭐⭐⭐⭐⭐ 高吞吐量
扩展性 ⭐⭐⭐⭐⭐ 集群部署,水平扩展

适用场景:

  • 大规模订单系统
  • 对可靠性要求极高
  • 已有RabbitMQ基础设施

RocketMQ延迟消息

方案原理

RocketMQ原生支持延迟消息,提供18个延迟级别。

延迟级别

plain 复制代码
RocketMQ预设延迟级别:
1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

对应level:
level 1  = 1s
level 2  = 5s
level 3  = 10s
...
level 15 = 20m
level 16 = 30m  ← 订单超时使用这个级别
level 17 = 1h
level 18 = 2h

实现步骤

1. 发送延迟消息

java 复制代码
@Service
public class OrderService {
    
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    
    public Order createOrder(OrderRequest request) {
        // 1. 创建订单
        Order order = saveOrder(request);
        
        // 2. 发送延迟消息
        OrderDelayMessage message = new OrderDelayMessage();
        message.setOrderId(order.getId());
        message.setOrderNo(order.getOrderNo());
        
        // 发送延迟消息,延迟级别16 = 30分钟
        rocketMQTemplate.syncSend(
            "order-cancel-topic",
            MessageBuilder.withPayload(message).build(),
            3000,
            16  // 延迟级别
        );
        
        return order;
    }
}

2. 消费延迟消息

java 复制代码
@Component
@RocketMQMessageListener(
    consumerGroup = "order-cancel-consumer",
    topic = "order-cancel-topic"
)
public class OrderCancelConsumer implements RocketMQListener<OrderDelayMessage> {
    
    @Autowired
    private OrderCancelService orderCancelService;
    
    @Override
    public void onMessage(OrderDelayMessage message) {
        log.info("收到延迟消息: orderId={}", message.getOrderId());
        
        try {
            // 取消订单(幂等处理)
            orderCancelService.cancelOrder(message.getOrderId());
        } catch (Exception e) {
            log.error("处理订单取消失败", e);
            throw e;  // 抛出异常,消息会重试
        }
    }
}

3. 自定义延迟时间(高级特性)

java 复制代码
// RocketMQ 5.0+ 支持任意时间延迟
Message message = MessageBuilder
    .withPayload(orderMessage)
    .build();

// 设置延迟投递时间
long deliverTime = System.currentTimeMillis() + 30 * 60 * 1000;  // 30分钟后
message.getHeaders().put(MessageConst.PROPERTY_TIMER_DELIVER_MS, deliverTime);

rocketMQTemplate.syncSend("order-cancel-topic", message);

优缺点分析

维度 评价 说明
实现难度 ⭐⭐ 原生支持,使用简单
时效性 ⭐⭐⭐⭐⭐ 精确到秒级
可靠性 ⭐⭐⭐⭐⭐ 金融级可靠性
性能 ⭐⭐⭐⭐⭐ 百万级TPS
扩展性 ⭐⭐⭐⭐⭐ 天然支持分布式

适用场景:

  • 超大规模订单系统
  • 对性能和可靠性要求极高
  • 阿里云/腾讯云环境

时间轮算法

方案原理

基于Netty的HashedWheelTimer实现时间轮,将任务按时间分配到不同的槽位。

时间轮结构

plain 复制代码
               时间轮(TimeWheel)
               
          0    1    2    3    4    5    6    7
        ┌────┬────┬────┬────┬────┬────┬────┬────┐
        │ [] │ [] │ [*]│ [] │ [*]│ [] │ [] │ [*]│
        └────┴────┴────┴────┴────┴────┴────┴────┘
                     ↑
                   指针
                   
说明:
- 时间轮有8个槽位
- 每个槽位代表1秒
- 指针每秒转动一格
- 到达槽位时,执行该槽位的所有任务
- [*] 表示有任务的槽位

实现代码

java 复制代码
@Component
public class TimeWheelService {
    
    private final HashedWheelTimer timer;
    
    @Autowired
    private OrderCancelService orderCancelService;
    
    public TimeWheelService() {
        // 创建时间轮
        // tickDuration: 每个槽位时间间隔 = 1秒
        // ticksPerWheel: 时间轮槽位数 = 3600 (1小时)
        this.timer = new HashedWheelTimer(
            1, TimeUnit.SECONDS,
            3600
        );
    }
    
    public void addCancelTask(Long orderId, int delayMinutes) {
        timer.newTimeout(timeout -> {
            // 取消订单
            orderCancelService.cancelOrder(orderId);
        }, delayMinutes, TimeUnit.MINUTES);
    }
    
    @PreDestroy
    public void destroy() {
        timer.stop();
    }
}

优缺点分析

维度 评价 说明
实现难度 ⭐⭐⭐⭐ 需要深入理解时间轮算法
时效性 ⭐⭐⭐⭐ 秒级精确
可靠性 ⭐⭐ 内存存储,服务重启丢失
性能 ⭐⭐⭐⭐⭐ O(1)时间复杂度
扩展性 ⭐⭐ 单机方案,难以扩展

适用场景:

  • 小规模系统
  • 对数据丢失容忍度高
  • 追求极致性能

方案选型对比

方案 实现难度 时效性 可靠性 性能 扩展性 成本 推荐指数
定时任务扫描 ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐
Redis过期监听 ⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐
Redis Sorted Set ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
RabbitMQ延迟队列 ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
RocketMQ延迟消息 ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
时间轮算法 ⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐

小型项目 (< 10万单/天)

  • 推荐: 定时任务扫描
  • 理由: 实现简单,无需额外组件,数据量小扫描效率高

中型项目 (10-100万单/天)

  • 推荐: Redis Sorted Set + 定时任务兜底
  • 理由: 时效性好(秒级),Redis普及率高,双重保障可靠性

大型项目 (100-500万单/天)

  • 推荐: RabbitMQ/RocketMQ延迟消息
  • 理由: 高性能、高可靠,成熟的消息中间件,天然支持分布式

超大型项目 (> 500万单/天)

  • 推荐: RocketMQ延迟消息 + Redis缓存
  • 理由: 百万级TPS,金融级可靠性,完善的监控体系
相关推荐
趁月色小酌***2 小时前
JAVA 知识点总结2
java·开发语言
虾说羊2 小时前
java中的代理详解
java
野生技术架构师2 小时前
2025年Java面试八股文大全(附PDF版)
java·面试·pdf
Coder_Boy_2 小时前
SpringAI与LangChain4j的智能应用-(实践篇4)
java·人工智能·spring boot·langchain
CC.GG2 小时前
【Qt】常用控件----QWidget属性
java·数据库·qt
资生算法程序员_畅想家_剑魔2 小时前
Java常见技术分享-13-多线程安全-锁机制-底层核心实现机制
java·开发语言
萤丰信息3 小时前
数智重构生态:智慧园区引领城市高质量发展新范式
java·大数据·人工智能·安全·智慧城市
用户3521802454753 小时前
🎉Spring Boot 3 + 多数据源 + Druid:监控页面 + 控制台 SQL 日志,终于搞定啦!
spring boot·微服务