在付款情景中,订单支付超时自动取消 是核心基础功能。本文将结合实战源码,深度讲解基于 Redisson 实现的 Redis 延迟队列方案,解析代码逻辑、核心原理,并解答一个高频疑问:为什么这个方案不能做成监听器模式?
一、业务背景
用户下单后,若 30 分钟内未完成支付,系统需要自动关闭订单、释放库存 。为了保证数据安全,必须满足:订单事务提交成功后,再创建超时任务(避免订单保存失败,却执行了取消任务的 BUG)。
本方案采用:Spring 事务同步 + Redisson 延迟队列 实现,轻量、可靠、开箱即用。
二、核心组件介绍
整个方案由 4 个核心部分组成,分工明确:
- 任务实体 :
OrderPaymentTimeoutTask存储订单 ID、订单号、超时时间(仅传递数据,无业务逻辑); - 调度器 :
scheduleAfterCommit事务提交后,将任务加入 Redis 延迟队列; - Redis 队列 :分为延迟队列 (计时等待)+阻塞队列(超时后存放待执行任务);
- 消费者 :
OrderPaymentTimeoutConsumer后台线程监听阻塞队列,执行取消订单逻辑。
三、核心源码详解
1. 任务实体(数据载体)
public record OrderPaymentTimeoutTask(
Long orderId, // 订单ID
String orderNo, // 订单编号
LocalDateTime payExpireTime // 支付超时时间
) {}
- 作用:仅存储超时任务所需的核心数据;
- 执行逻辑:消费者拿到订单 ID 后,固定执行取消未支付订单。
2. 调度器(事务安全入队)
public void scheduleAfterCommit(OrderDO order) {
// 1. 参数校验 + 功能开关判断
if (order == null || order.getId() == null || order.getPayExpireTime() == null) return;
if (!properties.isTimeoutDelayQueueEnabled()) return;
OrderPaymentTimeoutTask task = new OrderPaymentTimeoutTask(order.getId(), order.getOrderNo(), order.getPayExpireTime());
// 2. 核心:无事务直接调度,有事务则等待提交后调度
if (!TransactionSynchronizationManager.isActualTransactionActive()) {
schedule(task, calculateDelay(order.getPayExpireTime()));
return;
}
// 3. 事务提交回调(Spring 回调机制)
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
schedule(task, calculateDelay(order.getPayExpireTime()));
}
});
}
核心亮点:
- 利用 Spring 事务同步器,事务提交成功后才创建任务,杜绝数据不一致;
afterCommit()是典型的回调机制:事务完成后,自动调用我们预设的逻辑。
3. 队列入队逻辑(序列化 + Redis 存储)
public void schedule(OrderPaymentTimeoutTask task, Duration delay) {
// 任务序列化:Redis 无法存储Java对象,必须转为JSON字符串
String taskJson = JSONObject.toJSONString(task);
// 加入延迟队列,时间到达后自动移入阻塞队列
delayedQueue.offer(taskJson, actualDelay.toMillis(), TimeUnit.MILLISECONDS);
}
关键知识点:
- 序列化:Redis 只支持字符串 / 字节数组,对象必须转 JSON;
- 队列机制:延迟队列计时 → 时间到 → 任务自动移入阻塞队列。
4. 消费者(后台线程执行取消逻辑)
@PostConstruct
public void start() {
// 单线程后台线程,守护线程(服务关闭自动销毁)
executorService = Executors.newSingleThreadExecutor(r -> {
Thread thread = new Thread(r, "order-payment-timeout-consumer");
thread.setDaemon(true);
return thread;
});
// 启动循环监听
executorService.submit(this::consumeLoop);
}
// 核心监听循环
private void consumeLoop() {
RBlockingQueue<String> blockingQueue = redissonClient.getBlockingQueue(queueName);
while (running) {
// 阻塞获取任务:无任务则休眠,不消耗CPU
String payload = blockingQueue.take();
OrderPaymentTimeoutTask task = JSON.parseObject(payload, OrderPaymentTimeoutTask.class);
// 执行订单取消逻辑
orderPaymentSupportService.handlePaymentTimeout(task);
}
}
核心逻辑:
- 项目启动自动创建后台线程;
blockingQueue.take()阻塞等待任务,有任务立即执行;- 单线程保证任务顺序执行,避免并发问题。
四、高频灵魂拷问:为什么消费者不做成监听器?
很多开发者会疑惑:既然是监听队列,为什么不用注解式监听器(如 @RabbitListener),反而要写死循环?
1. 底层限制:Redis 不支持监听器模式
- 专业消息队列(RabbitMQ/RocketMQ) :支持主动推送消息,有成熟的监听器机制,消息到达后自动回调;
- Redis :仅作为缓存 / 存储,无消息推送能力,无法主动通知服务。
2. 现有方案是 Redis 队列的标准实现
Redis 阻塞队列的唯一使用方式:开启线程主动调用 take() 阻塞等待。
- 无任务时,线程休眠,CPU 占用极低;
- 有任务时,立即唤醒执行,性能与监听器一致。
3. 总结
不是不想做监听器,是 Redis 底层不支持! 当前 while 循环 + take() 阻塞监听,是 Redis 延迟队列官方、标准、最优的写法。
五、核心知识点总结
- 回调机制 :
afterCommit事务提交回调,保证任务与事务一致性; - 序列化:Redis 不支持 Java 对象,必须转为 JSON 字符串存储;
- 双队列设计 :
- 延迟队列:负责计时等待;
- 阻塞队列:时间到达后存放任务,供消费者消费;
- 监听模式:Redis 无推送能力,必须用后台线程阻塞监听,这是标准实现;
- 专用性 :当前代码为订单超时取消定制,仅支持这一种任务,不通用。
六、适用场景
- 中小型电商系统;
- 轻量级延迟任务(订单超时、验证码过期等);
- 不想部署重型 MQ,追求极简架构。
本方案无需独立部署中间件,基于 Redis 即可实现可靠的延迟任务,是中小项目的最优选择!