Appearance
JAVA
面试官:订单30分钟未支付自动取消,用户在第29分59秒付了钱,取消和支付回调同时到达,怎么处理?
核心问题:订单超时取消任务和支付回调并发执行,产生「幽灵订单」——数据库显示已支付但业务已取消,库存已释放。
❌ 错误答法
先查订单状态,如果是未支付就改成已取消/已支付,谁先到谁执行。
为什么错:这是典型的**检查后执行(TOCTOU)**竞态问题。取消任务查到「未支付」,支付回调查到也是「未支付」,取消任务先 update 成「已取消」,支付回调紧接着 update 覆盖成「已支付」,数据库状态和业务状态不一致。
✅ 正确方案:并发三道防线
第一道防线 — 数据库乐观锁(解决并发覆盖)
核心原则:永远不要相信在内存里查到的状态。
sql
UPDATE orders
SET status = 'PAID', pay_time = NOW()
WHERE order_id = ? AND status = 'UNPAID';将前置条件 status = 'UNPAID' 带进 WHERE 子句,数据库行锁会排队执行:
- 先抢到锁的更新成功,影响行数 = 1
- 后到的因为条件不满足,影响行数 = 0,直接返回
从根源杜绝覆盖写。
第二道防线 — 二次查库(解决幂等)
UPDATE 返回 0 时,不能直接发起退款(考虑支付宝回调重试机制,可能第二次回调时状态已是已支付)。
UPDATE 返回 0 → 二次查库:
- 状态已支付 → 直接返回成功(幂等处理)
- 状态已取消 → 走异常处理
第三道防线 — 异步退款 + 订单捞回(保障体验)
- 拒绝同步退款:回调接口里不直接调退款 API,用 MQ 异步处理,避免接口超时拖垮服务
- 业务容错思维:用户晚了几秒支付,优先捞回订单成交,而不是冷冰冰退款——把钱收进来永远比推出去更重要
- 分布式防抖:多节点部署时,超时任务可能同时唤醒多台机器,应用层加 Redis 分布式锁,只让一个节点执行取消,其余直接防抖
防线总结
| 防线 | 解决什么 | 手段 |
|---|---|---|
| 第一道 | 并发覆盖 | 数据库乐观锁(WHERE status = 'UNPAID') |
| 第二道 | 幂等性 | UPDATE 返回 0 后二次查库确认状态 |
| 第三道 | 体验保障 | MQ 异步退款 + 订单捞回 + Redis 分布式防抖 |
面试要点:面试考的不是 API 怎么调,而是在极端场景下能不能守住系统的底线——并发安全、幂等、容错、防抖,四层思维缺一不可。