标题: 订单超时不处理?用户体验和库存都炸了!
副标题: 四种方案实现订单自动取消,总有一款适合你
🎬 开篇:一个被遗忘的订单
双11购物狂欢:
00:00 - 用户A下单抢购iPhone(库存扣减)
00:15 - 用户A去洗澡了,忘记支付...
06:00 - 运营查看后台:
"怎么有1000个订单都是'待支付'状态?"
"库存全被锁住了!"
"其他用户买不了了!"
12:00 - 客服电话被打爆:
"为什么显示有库存却买不了?"
"我的订单怎么还是待支付?"
运营:这些超时订单为什么不自动取消?💀
开发:我...我忘了做这个功能...😰
损失:
- 库存占用:1000件
- 用户流失:500+
- 加班处理:通宵
- 老板暴怒:无价
教训:订单超时自动取消是电商系统的必备功能!
🤔 为什么需要自动取消?
想象你预订了餐厅座位:
- ❌ 不限时: 你订了不去,别人也订不了(浪费资源)
- ✅ 15分钟规则: 15分钟不到,自动释放座位(合理利用)
核心目的:释放锁定的库存,让其他用户能购买!
📚 知识地图
订单自动取消四大方案
├── ⏰ 方案1:定时任务扫描(简单但不精确)
├── 📦 方案2:延迟消息(推荐!)
├── ⏱️ 方案3:时间轮算法(高性能)
└── 🔔 方案4:Redis过期监听(轻量级)
⏰ 方案1:定时任务扫描
🌰 生活中的例子
保安巡逻:
- 每小时巡查一次,发现超时的车就开罚单
- 优点: 简单易实现
- 缺点: 不够及时,有延迟
💻 技术实现
/**
* 定时任务扫描方案
*/
@Component
public class OrderCancelScheduler {
@Autowired
private OrderService orderService;
@Autowired
private StockService stockService;
/**
* 定时扫描超时订单(每分钟执行一次)
*/
@Scheduled(cron = "0 */1 * * * ?")
public void scanAndCancelTimeoutOrders() {
log.info("开始扫描超时订单");
// 1. 查询超时的待支付订单
// 超时时间:15分钟
LocalDateTime expireTime = LocalDateTime.now().minusMinutes(15);
List<Order> timeoutOrders = orderService.findTimeoutOrders(expireTime);
log.info("查询到{}个超时订单", timeoutOrders.size());
// 2. 批量取消订单
for (Order order : timeoutOrders) {
try {
cancelOrder(order);
} catch (Exception e) {
log.error("取消订单失败:orderId=" + order.getId(), e);
}
}
log.info("超时订单扫描完成");
}
/**
* 取消订单
*/
@Transactional(rollbackFor = Exception.class)
public void cancelOrder(Order order) {
// 1. 更新订单状态
order.setStatus(OrderStatus.CANCELLED);
order.setCancelTime(LocalDateTime.now());
order.setCancelReason("超时未支付");
orderService.updateById(order);
// 2. 回滚库存
stockService.rollbackStock(order.getProductId(), order.getQuantity());
// 3. 如果有优惠券,回滚优惠券
if (order.getCouponId() != null) {
couponService.rollbackCoupon(order.getCouponId());
}
log.info("订单自动取消:orderId={}", order.getId());
}
}
/**
* Mapper实现
*/
@Mapper
public interface OrderMapper {
/**
* 查询超时订单
*/
@Select("SELECT * FROM `order` " +
"WHERE status = 'PENDING' " + // 待支付
"AND create_time < #{expireTime} " + // 超时
"LIMIT 1000") // 限制查询数量
List<Order> selectTimeoutOrders(@Param("expireTime") LocalDateTime expireTime);
}
/**
* 优点:
* ✅ 实现简单
* ✅ 易于理解和维护
* ✅ 无需额外组件
*
* 缺点:
* ❌ 时效性差(最多延迟1分钟)
* ❌ 数据库压力大(频繁全表扫描)
* ❌ 订单量大时性能差
*
* 适用场景:
* ✅ 订单量小(<10万/天)
* ✅ 对时效性要求不高
* ✅ 初期MVP版本
*/
优化版:分布式锁 + 分页查询
/**
* 优化的定时任务方案
*/
@Component
public class OptimizedOrderCancelScheduler {
@Autowired
private RedissonClient redissonClient;
@Autowired
private OrderService orderService;
@Scheduled(cron = "0 */1 * * * ?")
public void scanAndCancelTimeoutOrders() {
String lockKey = "order:cancel:lock";
RLock lock = redissonClient.getLock(lockKey);
try {
// ⚡ 获取分布式锁(避免多实例重复执行)
boolean locked = lock.tryLock(0, 50, TimeUnit.SECONDS);
if (!locked) {
log.info("获取锁失败,跳过本次执行");
return;
}
// 分页查询(避免一次加载过多数据)
int pageSize = 100;
int pageNum = 0;
while (true) {
List<Order> orders = orderService.findTimeoutOrdersPage(
pageNum++, pageSize);
if (orders.isEmpty()) {
break;
}
// 批量取消
orderService.batchCancelOrders(orders);
log.info("取消第{}页,共{}个订单", pageNum, orders.size());
}
} catch (InterruptedException e) {
log.error("获取锁被中断", e);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
📦 方案2:延迟消息(推荐!)
🌰 生活中的例子
外卖订单:
- 下单时设置15分钟闹钟
- 15分钟后闹钟响起,检查是否支付
- 未支付自动取消
💻 技术实现
实现1:RabbitMQ延迟队列
/**
* RabbitMQ延迟队列配置
*/
@Configuration
public class RabbitMQConfig {
/**
* 延迟交换机
*/
@Bean
public CustomExchange delayExchange() {
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");
return new CustomExchange(
"order.delay.exchange",
"x-delayed-message",
true,
false,
args
);
}
/**
* 延迟队列
*/
@Bean
public Queue delayQueue() {
return new Queue("order.cancel.queue", true);
}
/**
* 绑定
*/
@Bean
public Binding delayBinding(Queue delayQueue, CustomExchange delayExchange) {
return BindingBuilder
.bind(delayQueue)
.to(delayExchange)
.with("order.cancel")
.noargs();
}
}
/**
* 订单服务
*/
@Service
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 创建订单
*/
@Transactional(rollbackFor = Exception.class)
public String createOrder(OrderDTO dto) {
// 1. 创建订单
Order order = Order.builder()
.orderNo(generateOrderNo())
.userId(dto.getUserId())
.productId(dto.getProductId())
.quantity(dto.getQuantity())
.totalAmount(dto.getTotalAmount())
.status(OrderStatus.PENDING)
.createTime(LocalDateTime.now())
.build();
orderMapper.insert(order);
// 2. 扣减库存
stockService.deductStock(dto.getProductId(), dto.getQuantity());
// 3. ⚡ 发送延迟消息(15分钟后执行)
OrderCancelMessage message = OrderCancelMessage.builder()
.orderId(order.getId())
.orderNo(order.getOrderNo())
.build();
rabbitTemplate.convertAndSend(
"order.delay.exchange",
"order.cancel",
message,
msg -> {
// 设置延迟时间(毫秒)
msg.getMessageProperties().setDelay(15 * 60 * 1000); // 15分钟
return msg;
}
);
log.info("订单创建成功,已发送延迟取消消息:orderNo={}", order.getOrderNo());
return order.getOrderNo();
}
}
/**
* MQ消费者
*/
@Component
public class OrderCancelConsumer {
@Autowired
private OrderService orderService;
/**
* 消费延迟消息
*/
@RabbitListener(queues = "order.cancel.queue")
public void handleOrderCancel(OrderCancelMessage message) {
log.info("收到延迟消息:orderId={}", message.getOrderId());
try {
// 1. 查询订单
Order order = orderService.getById(message.getOrderId());
if (order == null) {
log.warn("订单不存在:orderId={}", message.getOrderId());
return;
}
// 2. 检查订单状态
if (order.getStatus() != OrderStatus.PENDING) {
log.info("订单已支付或已取消,无需处理:orderId={}, status={}",
order.getId(), order.getStatus());
return;
}
// 3. ⚡ 取消订单
orderService.cancelOrder(order);
log.info("订单自动取消成功:orderNo={}", order.getOrderNo());
} catch (Exception e) {
log.error("处理订单取消失败", e);
// 可以选择重试或记录到死信队列
throw new AmqpRejectAndDontRequeueException("处理失败", e);
}
}
}
/**
* 优点:
* ✅ 时效性好(精确到毫秒)
* ✅ 无需轮询数据库
* ✅ 性能高
* ✅ 可靠性高(消息持久化)
*
* 缺点:
* ⚠️ 需要RabbitMQ延迟插件
* ⚠️ 增加系统复杂度
*
* 适用场景:
* ✅ 订单量大(>10万/天)
* ✅ 对时效性要求高
* ✅ 生产环境推荐 ⭐⭐⭐⭐⭐
*/
实现2:RocketMQ延迟消息
/**
* RocketMQ延迟消息方案
*/
@Service
public class RocketMQOrderService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 创建订单
*/
@Transactional(rollbackFor = Exception.class)
public String createOrder(OrderDTO dto) {
// 1. 创建订单
Order order = createOrderEntity(dto);
orderMapper.insert(order);
// 2. 扣减库存
stockService.deductStock(dto.getProductId(), dto.getQuantity());
// 3. ⚡ 发送延迟消息
OrderCancelMessage message = OrderCancelMessage.builder()
.orderId(order.getId())
.orderNo(order.getOrderNo())
.build();
// RocketMQ支持18个延迟级别:
// 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
// 15分钟 = 延迟级别14
rocketMQTemplate.syncSend(
"order-cancel-topic",
MessageBuilder.withPayload(message).build(),
3000, // 发送超时
14 // 延迟级别:15分钟
);
log.info("订单创建成功,已发送延迟消息:orderNo={}", order.getOrderNo());
return order.getOrderNo();
}
}
/**
* RocketMQ消费者
*/
@Component
@RocketMQMessageListener(
topic = "order-cancel-topic",
consumerGroup = "order-cancel-consumer"
)
public class OrderCancelListener implements RocketMQListener<OrderCancelMessage> {
@Autowired
private OrderService orderService;
@Override
public void onMessage(OrderCancelMessage message) {
log.info("收到延迟消息:orderId={}", message.getOrderId());
// 查询订单并取消
Order order = orderService.getById(message.getOrderId());
if (order != null && order.getStatus() == OrderStatus.PENDING) {
orderService.cancelOrder(order);
}
}
}
⏱️ 方案3:时间轮算法
🌰 生活中的例子
手表的秒针:
- 秒针转一圈60秒
- 每个刻度代表1秒
- 到时间就执行任务
💻 技术实现
/**
* 时间轮实现(Netty Hashedwheeltimer)
*/
@Component
public class TimeWheelOrderCancel {
private final HashedWheelTimer timer;
@Autowired
private OrderService orderService;
public TimeWheelOrderCancel() {
// 创建时间轮
// tickDuration: 每格时间
// ticksPerWheel: 总格数
this.timer = new HashedWheelTimer(
new ThreadFactoryBuilder()
.setNameFormat("order-cancel-timer-%d")
.build(),
1, // 每格1秒
TimeUnit.SECONDS,
60 // 60格
);
timer.start();
}
/**
* 添加取消任务
*/
public void addCancelTask(Long orderId, long delaySeconds) {
timer.newTimeout(timeout -> {
// 延迟任务执行
try {
Order order = orderService.getById(orderId);
if (order != null && order.getStatus() == OrderStatus.PENDING) {
orderService.cancelOrder(order);
log.info("订单自动取消:orderId={}", orderId);
}
} catch (Exception e) {
log.error("取消订单失败:orderId=" + orderId, e);
}
}, delaySeconds, TimeUnit.SECONDS);
log.info("添加延迟取消任务:orderId={}, delay={}s", orderId, delaySeconds);
}
/**
* 关闭时间轮
*/
@PreDestroy
public void shutdown() {
if (timer != null) {
timer.stop();
}
}
}
/**
* 订单服务
*/
@Service
public class OrderService {
@Autowired
private TimeWheelOrderCancel timeWheelOrderCancel;
/**
* 创建订单
*/
@Transactional(rollbackFor = Exception.class)
public String createOrder(OrderDTO dto) {
// 1. 创建订单
Order order = createOrderEntity(dto);
orderMapper.insert(order);
// 2. 扣减库存
stockService.deductStock(dto.getProductId(), dto.getQuantity());
// 3. ⚡ 添加时间轮任务(15分钟后执行)
timeWheelOrderCancel.addCancelTask(order.getId(), 15 * 60);
return order.getOrderNo();
}
}
/**
* 优点:
* ✅ 性能极高(内存操作)
* ✅ 时效性好
* ✅ 适合大量短时任务
*
* 缺点:
* ❌ 任务存在内存中(重启丢失)
* ❌ 不支持持久化
* ❌ 单机方案(不支持分布式)
*
* 适用场景:
* ✅ 超高并发场景
* ✅ 短时延迟任务(<1小时)
* ✅ 可容忍任务丢失
*/
🔔 方案4:Redis过期监听
💻 技术实现
/**
* Redis过期监听配置
*/
@Configuration
public class RedisConfig {
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(
RedisConnectionFactory connectionFactory,
OrderExpireListener orderExpireListener) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 监听过期事件
container.addMessageListener(
orderExpireListener,
new PatternTopic("__keyevent@0__:expired")
);
return container;
}
}
/**
* 订单过期监听器
*/
@Component
public class OrderExpireListener implements MessageListener {
@Autowired
private OrderService orderService;
@Override
public void onMessage(Message message, byte[] pattern) {
String expiredKey = message.toString();
log.info("Redis key过期:{}", expiredKey);
// 解析key:order:cancel:123456
if (expiredKey.startsWith("order:cancel:")) {
String orderId = expiredKey.substring("order:cancel:".length());
try {
// 取消订单
Order order = orderService.getById(Long.parseLong(orderId));
if (order != null && order.getStatus() == OrderStatus.PENDING) {
orderService.cancelOrder(order);
log.info("订单自动取消:orderId={}", orderId);
}
} catch (Exception e) {
log.error("取消订单失败:orderId=" + orderId, e);
}
}
}
}
/**
* 订单服务
*/
@Service
public class OrderService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 创建订单
*/
@Transactional(rollbackFor = Exception.class)
public String createOrder(OrderDTO dto) {
// 1. 创建订单
Order order = createOrderEntity(dto);
orderMapper.insert(order);
// 2. 扣减库存
stockService.deductStock(dto.getProductId(), dto.getQuantity());
// 3. ⚡ 设置Redis key(15分钟后过期)
String key = "order:cancel:" + order.getId();
redisTemplate.opsForValue().set(
key,
order.getOrderNo(),
Duration.ofMinutes(15)
);
log.info("订单创建成功,已设置Redis过期监听:orderNo={}", order.getOrderNo());
return order.getOrderNo();
}
}
/**
* ⚠️ 注意:Redis过期监听的坑
*
* 1. 需要在redis.conf中开启:
* notify-keyspace-events Ex
*
* 2. 过期事件不保证立即触发
* - Redis采用惰性删除+定期删除
* - 可能延迟几秒
*
* 3. 集群模式下可能丢失
* - 主从切换时可能丢失
* - 需要配合消息队列使用
*
* 4. 性能问题
* - 大量key过期时性能下降
*
* 优点:
* ✅ 实现简单
* ✅ 轻量级
*
* 缺点:
* ❌ 不可靠(可能丢失)
* ❌ 不精确(可能延迟)
* ❌ 集群模式问题多
*
* 适用场景:
* ⚠️ 不推荐生产环境使用
* ✅ 可用于辅助方案
*/
📊 方案对比
| 方案 | 时效性 | 可靠性 | 性能 | 复杂度 | 推荐度 |
|---|---|---|---|---|---|
| 定时任务扫描 | 低(延迟1分钟) | 高 | 低 | 低 | ⭐⭐ |
| 延迟消息(RabbitMQ) | 高(毫秒级) | 高 | 高 | 中 | ⭐⭐⭐⭐⭐ |
| 延迟消息(RocketMQ) | 高 | 高 | 高 | 中 | ⭐⭐⭐⭐⭐ |
| 时间轮 | 高 | 低 | 极高 | 中 | ⭐⭐⭐ |
| Redis过期监听 | 中 | 低 | 中 | 低 | ⭐⭐ |
✅ 最佳实践
生产环境推荐方案:延迟消息(RabbitMQ/RocketMQ)
设计要点:
□ 订单创建时立即发送延迟消息
□ 消息体包含订单ID和订单号
□ 消费端先检查订单状态再取消
□ 幂等性设计(防止重复取消)
□ 记录取消日志(可追溯)
容错处理:
□ 消息消费失败重试3次
□ 最终失败记录死信队列
□ 人工介入处理死信消息
□ 定时任务作为兜底方案
性能优化:
□ 批量查询订单信息
□ 异步回滚库存
□ Redis缓存订单状态
□ 监控消息堆积情况
用户体验:
□ 支付前倒计时提示
□ 取消后push/短信通知
□ 提供一键恢复订单功能
□ 清晰的取消原因说明
🎯 完整实现示例
/**
* 完整的订单服务实现
*/
@Service
public class CompleteOrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StockService stockService;
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 创建订单
*/
@Transactional(rollbackFor = Exception.class)
public String createOrder(OrderDTO dto) {
// 1. 参数校验
validateOrderDTO(dto);
// 2. 检查库存
if (!stockService.checkStock(dto.getProductId(), dto.getQuantity())) {
throw new BusinessException("库存不足");
}
// 3. 创建订单
Order order = Order.builder()
.orderNo(generateOrderNo())
.userId(dto.getUserId())
.productId(dto.getProductId())
.quantity(dto.getQuantity())
.totalAmount(dto.getTotalAmount())
.status(OrderStatus.PENDING)
.createTime(LocalDateTime.now())
.expireTime(LocalDateTime.now().plusMinutes(15)) // 15分钟后过期
.build();
orderMapper.insert(order);
// 4. 扣减库存
boolean deductSuccess = stockService.deductStock(
dto.getProductId(),
dto.getQuantity()
);
if (!deductSuccess) {
throw new BusinessException("扣减库存失败");
}
// 5. 发送延迟消息(主方案)
sendDelayMessage(order.getId(), order.getOrderNo());
// 6. 设置Redis key(备用方案)
setRedisExpire(order.getId(), order.getOrderNo());
log.info("订单创建成功:orderNo={}", order.getOrderNo());
return order.getOrderNo();
}
/**
* 取消订单
*/
@Transactional(rollbackFor = Exception.class)
public boolean cancelOrder(Long orderId) {
// 1. 查询订单
Order order = orderMapper.selectById(orderId);
if (order == null) {
log.warn("订单不存在:orderId={}", orderId);
return false;
}
// 2. 幂等性检查
if (order.getStatus() != OrderStatus.PENDING) {
log.info("订单状态不是待支付,无需取消:orderId={}, status={}",
orderId, order.getStatus());
return false;
}
// 3. 更新订单状态
order.setStatus(OrderStatus.CANCELLED);
order.setCancelTime(LocalDateTime.now());
order.setCancelReason("超时未支付");
int updated = orderMapper.updateById(order);
if (updated == 0) {
log.error("更新订单状态失败:orderId={}", orderId);
return false;
}
// 4. 回滚库存
stockService.rollbackStock(order.getProductId(), order.getQuantity());
// 5. 回滚优惠券(如果有)
if (order.getCouponId() != null) {
couponService.rollbackCoupon(order.getCouponId());
}
// 6. 删除Redis key
String redisKey = "order:cancel:" + orderId;
redisTemplate.delete(redisKey);
// 7. 发送取消通知
sendCancelNotification(order);
log.info("订单取消成功:orderNo={}", order.getOrderNo());
return true;
}
/**
* 支付成功(取消定时任务)
*/
@Transactional(rollbackFor = Exception.class)
public void paySuccess(String orderNo) {
// 1. 查询订单
Order order = orderMapper.selectByOrderNo(orderNo);
if (order == null) {
throw new BusinessException("订单不存在");
}
// 2. 更新订单状态
order.setStatus(OrderStatus.PAID);
order.setPayTime(LocalDateTime.now());
orderMapper.updateById(order);
// 3. ⚡ 删除Redis key(阻止自动取消)
String redisKey = "order:cancel:" + order.getId();
redisTemplate.delete(redisKey);
// 注意:延迟消息已发送无法撤回
// 消费端会检查订单状态,已支付的订单不会被取消
log.info("订单支付成功:orderNo={}", orderNo);
}
/**
* 发送延迟消息
*/
private void sendDelayMessage(Long orderId, String orderNo) {
OrderCancelMessage message = OrderCancelMessage.builder()
.orderId(orderId)
.orderNo(orderNo)
.build();
rabbitTemplate.convertAndSend(
"order.delay.exchange",
"order.cancel",
message,
msg -> {
msg.getMessageProperties().setDelay(15 * 60 * 1000); // 15分钟
return msg;
}
);
}
/**
* 设置Redis过期key
*/
private void setRedisExpire(Long orderId, String orderNo) {
String key = "order:cancel:" + orderId;
redisTemplate.opsForValue().set(
key,
orderNo,
Duration.ofMinutes(15)
);
}
/**
* 发送取消通知
*/
private void sendCancelNotification(Order order) {
// 发送push通知
pushService.sendPush(
order.getUserId(),
"订单已取消",
"您的订单" + order.getOrderNo() + "已超时未支付,已自动取消"
);
// 发送短信(可选)
// smsService.sendSms(user.getPhone(), "订单取消通知", ...);
}
}
🎉 总结
核心要点
订单自动取消实现方案:
1️⃣ 生产环境首选:延迟消息
- RabbitMQ延迟插件
- RocketMQ延迟消息
- 时效性好,可靠性高
2️⃣ 备用方案:定时任务
- 作为兜底方案
- 处理消息丢失的情况
3️⃣ 关键设计点:
- 订单创建时发送延迟消息
- 消费端检查订单状态
- 幂等性设计
- 回滚库存和优惠券
4️⃣ 用户体验:
- 支付前倒计时
- 取消后通知
- 提供恢复功能
记住:订单自动取消是"延迟消息+定时任务"双保险! ⏰
文档编写时间:2025年10月24日
作者:热爱电商的订单工程师
版本:v1.0
愿你的订单管理井井有条! ⏰✨