背景:
我们逛淘宝的时候,遇到想要的商品,支付的时候,会有时间的限制,我们在时间限制里面完成支付,就会出订单号。但是如果逾期了,就会关闭超时订单。下面说一下思路以及伪代码。
一:定时器实现
思路:
- 存储机制:在订单创建时记录超时时间字段
expire_time
- 定时轮询:每5秒扫描数据库查询未支付订单
- 索引优化:通过复合索引加速查询
- 批量处理:分页查询避免单次处理数据量过大
伪代码:
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<Order> 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<Order> 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<String> 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<MessageExt> {
@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<Order> 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.2示例:订单系统提交任务:“订单ID=123,30分钟后触发,回调地址为/order/xxxx
”。 -
任务存储
2.1所有任务按触发时间排序存储。 2.2常用存储方式:数据库表、Redis Sorted Set、时间轮算法。 -
任务调度
3.1定时扫描“即将触发”的任务. 3.2触发时,将任务发送到延迟队列或直接调用业务回调接口。 -
任务处理
4.1 触发后,超时中心调用业务系统的接口通知:“订单ID=123已超时,请处理”。 4.2业务系统自行实现具体逻辑(如关闭订单)。
优点
1,针对超时任务调度场景专项设计,通过分布式架构提高吞吐。
2,基于时间轮、延迟队列等机制,可支撑千万级任务调度。
3,专人维护,质量好。
缺点
1,技术成本和维护成本高。
2,实现复杂度比较高,会涉及分布式调度,MQ,高存储(redis)等,面临分布式锁,任务分片,网络抖动,节点宕机一系列问题,并且运维复杂度增加。