连载中 9/15

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

2026-09-06 · 662 阅读 · 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)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10101 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9873 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9294 阅读 · 21 评论 · 287 赞