引言
在电商、外卖、票务等业务中,“下单后若 30 分钟未支付则自动取消”是一道经典需求。实现方式既要保证 实时性,又要在 高并发 下保持 低成本、高可靠。
本文基于 spring boot,给出 3 种生产级落地方案,并附完整代码与选型对比,方便快速决策。
一、需求拆解
| 功能点 | 约束 |
|---|---|
| 触发条件 | 创建时间 + 30 min 仍未支付 |
| 实时性 | 秒级(理想) / 分钟级(可接受) |
| 幂等 | 重复取消需幂等 |
| 高并发 | 峰值 10 w+/日 |
| 数据一致性 | 不能漏单、不能错单 |
二、方案总览
| 方案 | 核心机制 | 实时性 | 额外组件 | 代码复杂度 |
|---|---|---|---|---|
| ① 定时任务 | @scheduled + db 扫描 | 分钟级 | 无 | ★☆☆ |
| ② 延迟队列 | rabbitmq ttl + dlx | 秒级 | rabbitmq | ★★☆ |
| ③ redis 过期事件 | key ttl + keyspace notify | 秒级 | redis | ★★☆ |
三、方案 1:定时任务(@scheduled)
1. 思路
周期性扫描订单表,把“创建时间 + 30 min < 当前时间”且状态为 pending 的订单置为 cancelled。
2. 代码实现
@enablescheduling
@component
@requiredargsconstructor
public class ordercancelschedule {
private final orderservice orderservice;
/** 每 30s 跑一次,可根据数据量调整 */
@scheduled(fixeddelay = 30_000)
public void cancelunpaidorders() {
localdatetime expirepoint = localdatetime.now().minusminutes(30);
list<long> ids = orderservice.findunpaidbefore(expirepoint);
if (!ids.isempty()) {
int affected = orderservice.batchcancel(ids);
log.info("自动取消订单 {} 条", affected);
}
}
}
3. 优化技巧
- 分页 + 索引:
create index idx_order_status_created on t_order(status, created_time);
- 分片扫描:按 id 或时间分片,避免大表锁。
- 单机多线程:
@async("cancelexecutor")+ 线程池。
4. 优缺点
- ✅ 零依赖、实现快
- ❌ 数据量大时 db 压力大;实时性受轮询间隔限制
5. 适用场景
日订单 < 1 w,或作为兜底方案。
四、方案 2:rabbitmq 延迟队列
1. 思路
订单创建后发送一条 30 min ttl 的消息;到期自动路由到消费队列,消费者检查订单状态并取消。
2. 架构图
producer ──> delay exchange (x-delayed-message) ──> 30min ttl ──> cancel queue ──> consumer
3. 代码实现
3.1 声明交换机 & 队列
@configuration
public class rabbitdelayconfig {
@bean
public customexchange delayexchange() {
map<string, object> args = map.of("x-delayed-type", "direct");
return new customexchange("order.delay", "x-delayed-message", true, false, args);
}
@bean
public queue cancelqueue() {
return queuebuilder.durable("order.cancel.queue").build();
}
@bean
public binding binding() {
return bindingbuilder.bind(cancelqueue()).to(delayexchange()).with("order.cancel").noargs();
}
}
3.2 发送延迟消息
@service
@requiredargsconstructor
public class orderpublisher {
private final rabbittemplate rabbittemplate;
public void createorder(order order) {
// 1. 落库
ordermapper.insert(order);
// 2. 发送延迟消息
rabbittemplate.convertandsend(
"order.delay",
"order.cancel",
order.getid(),
msg -> {
msg.getmessageproperties().setdelay(30 * 60 * 1000); // 30 min
return msg;
}
);
}
}
3.3 消费并取消
@component
@rabbitlistener(queues = "order.cancel.queue")
public class cancelconsumer {
private final orderservice orderservice;
@rabbithandler
public void handle(long orderid) {
order order = orderservice.find(orderid);
if (order != null && order.getstatus() == orderstatus.pending) {
orderservice.cancel(orderid);
}
}
}
4. 优缺点
- ✅ 实时性好(秒级);支持分布式;消息持久化
- ❌ 需要 rabbitmq;链路更长
5. 适用场景
中高并发,需秒级取消,已用 mq 或愿意引入 mq。
五、方案 3:redis keyspace 过期事件
1. 思路
以 order:{id} 作为 key,30 min ttl;redis 键过期时推送事件;应用监听后取消订单。
2. redis 配置
# redis.conf notify-keyspace-events ex
或 cli:
config set notify-keyspace-events ex
3. 代码实现
3.1 订单创建时写 redis
@service
public class orderservice {
private final stringredistemplate redistemplate;
public void createorder(order order) {
ordermapper.insert(order);
// value 随意,这里用 id
redistemplate.opsforvalue()
.set("order:" + order.getid(),
string.valueof(order.getid()),
duration.ofminutes(30));
}
}
3.2 监听过期事件
@configuration
public class redislistenerconfig {
@bean
public redismessagelistenercontainer container(redisconnectionfactory cf) {
redismessagelistenercontainer container = new redismessagelistenercontainer();
container.setconnectionfactory(cf);
container.addmessagelistener(
(message, pattern) -> {
string key = message.tostring();
if (key.startswith("order:")) {
string orderid = key.substring(6);
// 幂等取消
orderservice.cancelifunpaid(long.valueof(orderid));
}
},
new patterntopic("__keyevent@*__:expired")
);
return container;
}
}
4. 幂等 & 可靠性
- 幂等:取消 sql 加状态条件
where status = pending。 - 可靠性:redis 重启会丢失未过期 key,需 兜底定时任务(方案 1)双保险。
5. 优缺点
- ✅ 实时性高,组件少
- ❌ redis 重启可能丢事件;需处理幂等
6. 适用场景
已用 redis,订单量中等,能接受极低概率漏单。
六、3 种方案对比与选型
| 维度 | 定时任务 | rabbitmq 延迟队列 | redis 过期事件 |
|---|---|---|---|
| 实时性 | 分钟级 | 秒级 | 秒级 |
| 吞吐量 | 低 | 高 | 中 |
| 额外组件 | 无 | rabbitmq | redis |
| 可靠性 | 高 | 高 | 中(需兜底) |
| 实现复杂度 | ★☆☆ | ★★☆ | ★★☆ |
| 推荐场景 | 小流量、兜底 | 高并发、已用 mq | 已用 redis、中等并发 |
建议:
- 小项目 → 定时任务即可;
- 大流量 → 延迟队列;
- 已用 redis → 过期事件 + 定时任务兜底双保险。
七、灰度 & 监控
- 灰度发布:按用户尾号或城市分批切换方案。
- 监控指标:
- 取消成功率
- mq 消息积压
- redis 过期 qps
- 定时任务扫描耗时
八、小结
一句话总结:定时任务 简单但慢;延迟队列 实时但重;redis 过期 轻量但需兜底。
在实际落地中,可以 并行运行 两种方案(如延迟队列 + 兜底定时任务),通过配置开关灵活切换,确保业务永远在线。祝你的订单永不超卖!
以上就是springboot订单超时自动取消的三种主流实现方案的详细内容,更多关于springboot订单超时自动取消的资料请关注代码网其它相关文章!
发表评论