支付回调与结算峰值
→ 返回 高并发
秒杀、12306、红包活动结束后,支付渠道会集中回调同一批订单。回调具有重复投递、乱序、突发特征,设计重点是幂等、快速 ACK、异步推进状态机。
典型问题
| 问题 | 后果 |
|---|---|
| 微信/支付宝重复通知 | 重复发货、重复加余额 |
| 回调处理慢 | 渠道超时重试 → 流量放大 |
| 回调与查单并发 | 订单状态竞态 |
| 结算、分账、开票堆在同一事务 | DB 锁、长事务 |
目标
- 对渠道:尽快返回 success(通常 < 1s)
- 对业务:恰好一次推进订单(幂等键)
- 结算 / 分账:异步,可最终一致
推荐流程
支付渠道 POST 回调
▼
网关:验签、解析 out_trade_no
▼
幂等层:INSERT notify_log (uniq: channel + notify_id) 或 Redis SETNX
▼
若已处理 → 直接 200 success
▼
否则:更新订单为「已支付」条件 UPDATE(status=待支付)
▼
发 MQ:order.paid → 库存确认 / 发货 / 积分 / 分账
▼
返回 200(业务失败也需谨慎:渠道重试策略不同)
核心解法
1. 幂等键选择
| 来源 | 幂等键 |
|---|---|
| 渠道通知 | notify_id / transaction_id |
| 业务 | order_id + pay_channel |
-- 示例:仅当待支付可推进
UPDATE orders SET status='PAID', paid_at=NOW()
WHERE order_id=? AND status='PENDING_PAY';
-- affected_rows=0 → 已处理或非法,仍记录日志2. 先落库再 ACK(或 Redis 去重 + 异步)
- 同步路径:验签 + 幂等表 + 订单状态 CAS(尽量无跨表大事务)
- 重逻辑:优惠券核销、分账、物流 — 全部走 MQ 消费者,可重试
3. 查单与回调统一入口
用户主动「我已支付」查单与渠道回调应走同一状态机服务,避免两套逻辑。
handlePaymentSuccess(orderId, source=callback|query, idempotentKey)4. 结算峰值削峰
| 手段 | 说明 |
|---|---|
| 按商户/日期分区消费 | 避免单队列打满 |
| 批量结算任务 | T+1 汇总而非每笔实时写多表 |
| 对账任务 | 渠道账单 vs 本地流水,修补差异 |
5. 超时关单与支付竞态
用户支付成功同时定时任务关单:
- 关单:
UPDATE ... WHERE status=PENDING AND expire_at<NOW() - 支付成功:仅
PENDING可转PAID - 若已
CLOSED且收到支付 → 走退款/异常单,不要静默丢钱
与秒杀 / 12306 的衔接
| 阶段 | 要点 |
|---|---|
| 下单占库存 | 已扣 Redis/DB 库存 |
| 待支付 | 短 TTL 订单 + 延迟关单 MQ |
| 支付回调 | 本页:幂等确认 |
| 关单 / 退款 | 回滚库存(与 秒杀与抢购 一致) |
检查清单
- 是否用渠道
notify_id做唯一约束? - 回调是否轻量快速 200?
- 发货/分账是否异步?
- 查单与回调是否同一状态机?
- 关单与支付成功是否有竞态处理与退款路径?
- 是否有日终对账?