连载中 9/15

异步下单:MQ 削峰与订单落库

2026-09-06 · 657 阅读 · 0 评论 · 0 赞

为什么不直接写库

预扣成功后如果直接写订单库,两千 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 跳过;存储层——订单表对订单号建唯一索引,重复插入直接报冲突捕获跳过。两层独立生效,任何一层失效另一层兜底。幂等不是可选项:至少一次投递下,没有幂等的消费端迟早制造重复订单。

失败与回补

落库失败的出口要明确:可重试错误(数据库抖动)走消息重投;不可重试错误(参数问题)进死信并触发库存回补——资格已扣但订单成不了,库存必须回到池子里,否则就是少卖。回补动作本身也要幂等,这是下一篇的起点:订单超时与库存回补的闭环

503

10 年全栈工程师 · 503咖啡馆主理人

#异步下单#MQ削峰#消息不丢#消费幂等#订单号

评论 (0)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 5 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞