为什么不直接写库
预扣成功后如果直接写订单库,两千 QPS 的瞬时写入照样把数据库打满——预扣只是筛掉了绝大多数流量,剩下的流量依然远超数据库的舒适区。MQ 的角色是削峰填谷:写请求先进队列排队,消费端按数据库消化能力(比如 500/s)平稳拉取落库——上游爆发,下游匀速。代价是订单不是立刻生效:用户看到的从"下单成功"变成"排队中,结果稍后通知"——秒杀场景完全能接受,抢到抢不到本来就要等。
订单号:预生成并透传全链路
订单号在预扣成功那一刻就生成(雪花 ID 或带路由基因的号段),随消息透传给落库、支付、通知、对账每一个环节——它是串联异步链路的主线:用户查询靠它,幂等去重靠它,对账核对靠它。如果等落库时才生成,前端轮询与消息之间就对不上号,幂等键也要另找,链路平白多出复杂度。
消息不丢的三段保证
异步链路的命门是消息丢失——Redis 扣了库存、消息却丢了,等于白白少卖。三段各有一道锁:生产端——发送确认,broker 落盘成功才算发送成功,失败则重发;存储端——消息持久化加副本,broker 单机故障不丢数据;消费端——手动 ack,处理成功才确认,处理失败重投。三段合起来是"至少一次"投递——而至少一次必然带来重复,重复的解决靠下一节。
// 消费端:幂等 + 落库
@RabbitListener(queues = "seckill.order")
public void onOrder(OrderMsg msg, Channel ch, Message m) {
try {
// 幂等第一层:预扣记录表查状态
if (deductRepo.done(msg.orderId)) { ch.basicAck(m); return; }
orderRepo.insertIfAbsent(build(msg)); // 幂等第二层:唯一索引兜底
deductRepo.markDone(msg.orderId);
notifyUser(msg.orderId, SUCCESS); // 推送"抢到了,去支付"
ch.basicAck(m);
} catch (Exception e) {
ch.basicNack(m, false, true); // 失败重投,交给重试与死信
}
}消费端幂等:两道防线
重复消息的防御分两层:业务层——预扣记录表记录每个订单号的扣减状态,已处理的直接 ack 跳过;存储层——订单表对订单号建唯一索引,重复插入直接报冲突捕获跳过。两层独立生效,任何一层失效另一层兜底。幂等不是可选项:至少一次投递下,没有幂等的消费端迟早制造重复订单。
失败与回补
落库失败的出口要明确:可重试错误(数据库抖动)走消息重投;不可重试错误(参数问题)进死信并触发库存回补——资格已扣但订单成不了,库存必须回到池子里,否则就是少卖。回补动作本身也要幂等,这是下一篇的起点:订单超时与库存回补的闭环。
评论 (0)