【JAVA架构师成长之路】【电商系统实战】第9集:订单超时关闭实战(Kafka延时队列 + 定时任务补偿)
30分钟课程:订单超时关闭实战(Kafka延时队列 + 定时任务补偿)
课程目标
- 理解订单超时关闭的业务场景与核心需求。
- 掌握基于 Kafka 延时队列与定时任务的关单方案设计。
- 实现高并发场景下的可靠关单逻辑(防重复、幂等性)。
课程内容与时间分配
0~5分钟:课程概述
业务场景与挑战
- 超时关单:用户下单后未支付,需在指定时间(如30分钟)后自动关闭订单并释放库存。
- 核心问题:
- 精准延迟:如何确保消息在指定时间后被消费?
- 可靠性:避免消息丢失或重复消费导致订单状态错误。
- 高性能:支撑每日百万级订单的关单需求。
技术选型
- Kafka 延时队列:利用 Kafka 时间轮(Timer Wheel)实现近似延迟(需外部存储辅助)。
- 定时任务补偿:兜底扫描未支付订单,防止 Kafka 消息丢失或延迟误差。
5~10分钟:技术难点与核心问题
- Kafka 原生不支持延时队列
- 需结合业务逻辑实现消息延迟投递(如按时间分桶)。
- 消息重复消费
- 网络抖动或消费者重启可能导致重复关单。
- 分布式系统时钟同步
- 多节点定时任务需避免重复扫描(分布式锁)。
- 数据一致性
- 关单需同时释放预扣库存、更新订单状态,需事务性保障。
10~25分钟:解决方案与代码实战
1. Kafka延时队列设计(10~15分钟)
方案设计
- 消息分桶:按延迟时间分多个 Topic(如 delay_5m、delay_30m)。
- 生产者逻辑:订单创建时发送消息到对应延迟 Topic。
- 消费者逻辑:监听延迟 Topic,到期后触发关单。
生产者代码(发送延迟消息)
@Service
public class OrderTimeoutProducer {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
// 发送延迟消息(按分钟分桶)
public void sendDelayMessage(String orderId, long delayMinutes) {
String topic = "delay_" + delayMinutes + "m";
kafkaTemplate.send(topic, orderId);
}
}
// 订单创建时调用(延迟30分钟)
orderTimeoutProducer.sendDelayMessage(orderId, 30);
消费者代码(处理关单)
@KafkaListener(topics = "delay_30m")
public void handleDelayMessage(String orderId) {
Order order = orderService.getOrder(orderId);
if (order.getStatus() == OrderStatus.UNPAID) {
orderService.closeOrder(orderId); // 关单逻辑(释放库存、更新状态)
}
}
2. 定时任务补偿(15~25分钟)
方案设计
- 兜底扫描:每小时扫描一次未支付且未关闭的订单(创建时间 > 30分钟)。
- 分布式锁:防止多节点重复扫描(Redis 锁)。
定时任务代码
@Scheduled(cron = "0 0/60 * * * ?") // 每小时执行一次
public void scanUnpaidOrders() {
String lockKey = "lock:scan_unpaid_orders";
// 获取分布式锁(Redis 实现)
if (redisLock.tryLock(lockKey, 60)) {
try {
// 查询超过30分钟未支付的订单
List<Order> orders = orderMapper.selectUnpaidOrders(30);
for (Order order : orders) {
orderService.closeOrder(order.getOrderId());
}
} finally {
redisLock.unlock(lockKey);
}
}
}
关单幂等性处理
public void closeOrder(String orderId) {
// 使用数据库乐观锁确保幂等性
int rows = orderMapper.updateOrderStatus(
orderId,
OrderStatus.UNPAID,
OrderStatus.CLOSED
);
if (rows > 0) {
stockService.rollbackStock(orderId); // 释放库存
}
}
25~30分钟:练习与拓展
练习题目
- 动态延迟配置
- 要求:支持不同商品类目设置不同的关单时间(如虚拟商品5分钟,实物商品30分钟)。
- 消息丢失补偿
- 场景:Kafka 消息丢失导致未触发关单。
- 任务:优化定时任务扫描逻辑,优先处理 Kafka 未覆盖的订单。
推荐拓展方向
- 精准延时队列
- 结合 RocketMQ 的延迟消息(支持18个固定延迟级别)。
- 分库分表优化
- 按订单创建时间分表,提升定时任务扫描效率。
- 重试队列设计
- 关单失败时,将订单ID投递到重试队列,最多重试3次。
课程总结
- 延时队列核心逻辑:Kafka 分桶 + 定时任务兜底,平衡性能与可靠性。
- 关键设计:
- 幂等性:通过数据库乐观锁防止重复关单。
- 分布式锁:避免定时任务多节点重复执行。
- 事务性:关单与库存释放需原子化(可结合本地事务表)。
- 适用场景:高并发、允许短暂误差的延迟任务(如订单关单、优惠券过期)。
课后资源
- Kafka 延时队列参考:Kafka Delayed Message Design
- 完整代码示例:GitHub - 电商关单系统Demo