在电商、订票或预约类系统中,订单的提交与取消是核心业务流程之一。为了保障系统的稳定性与用户体验,我们需要有效地处理两个常见问题:
- 订单重复提交(用户连续点击、页面卡顿、网络重试等引起)
- 订单超时未支付自动取消
本篇博客将介绍这两个问题产生的原因,以及在实际开发中常见的解决方案。
代码地址:https://github.com/RainbowJier/ArchitectureDesign#
架构设计
应用架构图
应用架构图,描述后端系统由哪些应用组成,对系统架构中的某个模块进行细分说明 [1],比如流量包下单模块的应用架构图,如下所示:
系统时序图
时序图说明 [2]:
- 获取 token:用户进入订单页面,生成 token 存储到 Redis。
- 用户下单
- 配置 Redis 加锁,实现防止用户重复提交。
- 订单价格验证
- 考虑优惠券或其它折扣因素
- 校验前端展示的价格与后端计算出的价格是否一致
- 创建订单信息,保存到数据库。
- 发送订单信息到延迟队列,在规定时间内未支付,自动关闭订单。
- 响应用户,下单成功。
- 集成第三方支付接口,响应第三方支付信息(关键点)
- 等待用户支付,回调处理,
- 支付成功
- 同步更新订单支付状态。
- 激活对应的流量套餐。
- 支付失败
- 超过规定支付时间,延迟队列自动关闭订单。
- 支付成功
订单重复提交防范机制
问题场景
用户在下单时可能回出现如下情况:
- 在前端下单过程中,由于用户多次点击提交按钮,导致系统重复接收并处理下单请求,进而生成了多个订单。
- 由于网络延迟或其他技术性因素造成的页面响应迟缓,促使用户反复尝试刷新页面或重新提交表单,从而无意中触发了多次请求的发送。
- 黑客或具有恶意意图的用户利用 Postman 等 HTTP 客户端工具,故意对系统进行重复性的表单提交攻击,以此达到破坏正常服务流程的目的。
问题:
- 导致表单重复提交,造成数据重复或者混乱。
- 核心接口的请求次数增加,消耗服务器负载,严重造成服务器宕机、
解决方案
幂等性设计
实现幂等性的核心是:无论请求被提交多少次,结果都是一样的。
❌方案一:前端控制
前端 JS 控制点击次数,屏蔽点击按钮无法点击
存在问题:
- 前端的 JS 可以被绕过。
✅方案二:服务端 token 令牌
架构设计
使用 SpringBoot 的 AOP 切面编程,每次调用订单接口时,会先校验是否“重复提交”。
流程图
-
用户每次进入订单页面
- 系统生成一个
order_token
,存储在 Redis, 如SETNX
(设置唯一标识)配合过期时间。
- 系统生成一个
-
用户提交订单时
- AOP 拦截请求,校验这个 order_token 是否存在。
- 提交订单时必须携带这个
order_token
校验是否 Redis 中是否存在键值对。
-
后端验证 token 是否存在
- 存在,说明首次下订单,保存订单,删除 Redis 中的
order_token
。 - 不存在,说明重复提交,无法保存订单。
- 存在,说明首次下订单,保存订单,删除 Redis 中的
-
用户进入下单页面
- 调用 getToken() 方法生成 request-token 令牌
{key: order:submit:[account_no]:[32 位随机数],value : 1}
- 存储到 Redis。
- 调用 getToken() 方法生成 request-token 令牌
-
用户提交订单时
- 进入 AOP 拦截器,校验 request-token 是否存在。
- 存在,不拦截请求,保存订单信息到数据库,删除 Redis 中的令牌。
- 不存在,删除失败,说明重复提交,直接拦截请求。
订单超时未支付自动取消
问题场景
- 支付超时判断:用户在下单之后,系统需要设置一个支付超时时间(如 20 分钟、1 小时等)。
- 订单状态变更:当支付超时后,系统需要将订单状态设置为”支付超时“或者”失效“, 并触发响应的业务逻辑。
- 通知用户:在订单超时前,可以通过短信、邮箱等通知用户。
解决方案
架构设计
- 为何订单的延迟消息时间会超过支付的有效时间❓
- 情形说明:若延迟消息设定的有效时间为 10 分钟,而支付的有效期为 20 分钟。
- 具体流程分析:
- 在延迟消息到期后的第 10 分钟,如果用户尚未完成支付,则“关单服务”自动介入,并将该订单的状态更新为“未支付”。
- 假设在 15 分钟时,用户尝试进行支付。此时,由于订单已被标记为“未支付”,即便用户成功支付,也无法即时反映至订单的实际状态中,从而导致了数据的一致性问题。
- **在“关单服务”流程中,为何需要在”关闭订单“时查询”第三档支付平台“**❓
a. 用户在完成第【6】步支付操作后,可能会由于网络问题导致虽然实际已付款,但系统数据库中的订单状态仍显示为未支付。这种情况会导致订单状态出现错误。
b. 因此,在第【9】步时,通过再次向第三方支付平台查询该笔交易的实际状态,可以准确确认订单的真实情况,从而保证数据的一致性和准确性。
RabbitMQ 实现延迟队列
RabbitMQ TTL+死信队列示意:
- 设置订单队列消息的 TTL 为 30 分钟
- 到期未消费进入死信队列
- 消费死信队列,执行取消逻辑
@Bean
public Queue orderDelayQueue() {Map<String, Object> args = new HashMap<>(3);args.put("x-message-ttl", ttl); // 设置消息过期时间args.put("x-dead-letter-exchange", orderCloseEventExchange);args.put("x-dead-letter-routing-key", orderReleaseBindingKey);return new Queue(orderDelayQueue, true, false, false, args);
}
实践建议
- 接口层防抖 + 服务层幂等 + 数据库唯一约束 三层保障,才能真正防止重复订单
- 对于高并发系统,延迟队列+死信机制 是更可控、更实时的取消策略
- 实时取消的同时,注意回滚库存、释放优惠券、通知用户等后续处理