景区票务系统的消息队列架构演进:从订单到对账的异步化改造实录|中科智旅景游宝
先说个让人头大的场景。每年五一、国庆,售票高峰期订单服务同步干的事特别多:创建订单、扣库存、发短信、发模板消息、更新会员积分、推送给 OTA 渠道、写财务流水。一个下单接口里串了 6 个逻辑,游客点一次购买,页面要等 2 到 3 秒才出结果。更恐怖的是,短信服务一抖动,整个下单流程跟着超时,游客以为支付失败,又下了一单------重复下单就是这么来的。
后来我们把下单链路做了异步化改造,核心就是引入消息队列(MQ)。今天把这次改造的完整过程讲清楚,从选型到落地到踩坑。
一、先理清:哪些逻辑该异步?哪些必须同步?
改造第一件事不是选 MQ,而是梳理同步/异步边界。判断标准很简单:游客付完钱,哪些结果是他立刻需要知道的?
必须同步的(游客感知的核心链路):
- 创建订单、锁定库存
- 调支付、收支付结果
- 返回订单号、支付状态给游客
必须异步的(游客不感知、但系统要做的事):
- 发短信、发微信模板消息(通知类,晚几秒无所谓)
- 更新会员积分(可以后补)
- 推送订单给 OTA 渠道(渠道对账可以延迟)
- 写财务流水、生成对账数据(财务可以日终对账)
- 更新数据驾驶舱的经营指标(本来就是准实时)
二、MQ 选型:Kafka 还是 RocketMQ?
市面主流就几个,我们对比后选了 RocketMQ,理由很实际:
| 对比项 | Kafka | RocketMQ | RabbitMQ |
|---|---|---|---|
| 吞吐量 | 极高 | 高 | 中 |
| 消息可靠性 | 高(需配置) | 高(自带事务消息) | 高 |
| 事务消息 | 不支持原生 | 支持原生 | 需插件 |
| 延迟 | 毫秒级 | 毫秒级 | 微秒级 |
| 运维复杂度 | 中 | 中 | 低 |
| 中文文档 | 一般 | 丰富 | 丰富 |
关键差异在事务消息:景区票务里"订单落库 + 发消息"要保证原子性,RocketMQ 原生支持事务消息,代码量少很多。加上团队之前没碰过 Kafka,学习成本也是考量。最终定了 RocketMQ 4.9。
三、改造过程:核心链路怎么拆?
1. 下单接口:同步只剩三步
改造后的下单流程:
游客点击购买
→ 创建订单(本地事务,落库)
→ 锁定库存(Redis + DB)
→ 调支付接口,返回支付二维码
→ 接口结束(响应时间从 2.5s 降到 400ms)
支付成功回调后,才把"后续动作"发到 MQ:
ORDER_PAID消息 → 发短信、模板消息ORDER_PAID消息 → 更新会员积分ORDER_PAID消息 → 推送 OTA 渠道ORDER_PAID消息 → 写财务流水
一个支付回调,拆成 4 个 topic 的异步任务,全部并行消费。
2. 消息可靠性:事务消息 + 重试 + 死信
异步化最怕丢消息,我们做了三层保障:
第一层:事务消息保证"业务和消息同生共死"
RocketMQ 事务消息的核心流程:先发半消息 → 本地事务执行成功 → 提交消息 → 消费者才可见。如果本地事务失败,半消息被回滚,消息也不会发出。这就保证了"订单落库成功,消息一定发出"。
第二层:消费失败重试
消费端处理失败自动重试(RocketMQ 默认重试 16 次,指数退避)。短信服务抖动导致消费失败,重试机制兜底,不会丢。
第三层:死信队列 + 人工补偿
重试 16 次还失败的消息进死信队列,我们有个补偿 Job 每小时扫一次死信,人工介入处理。财务流水这种关键消息,死信队列里绝不能躺过一天。
3. 消费幂等:上篇方案的复用
消息消费必须幂等(前面幂等篇讲过,这里直接复用同一套):消费端按消息 ID 去重,重复投递不重复处理。OTA 渠道推送这种场景,渠道侧可能重复回调,幂等键必加。
四、踩过的坑(血泪教训)
- 消费端业务代码太厚:第一版把"发短信 + 更新积分 + 推渠道"写在一个消费者里,一个环节报错整条消费失败重试,其他环节跟着重复执行。后来一个 topic 一个消费者,职责单一,各重试各的。
- 消费顺序错乱:OTA 渠道要"先创建后取消"的顺序,默认并发消费会乱序。改成单消费者 + 顺序消息(按订单号 hash 到同一队列),顺序问题解决。
- 消息堆积没监控:某个节假日渠道推送消费者挂了 2 小时,消息堆积了几十万条,没人发现。后来给每个 topic 加了"堆积量 > 1 万条告警",再没出现过这种事。
- 重试风暴:下游全挂时,重试全部在同一时间点打过去,把下游打得更死。改成指数退避 + 随机抖动,重试错开。
- 事务消息半消息积压:事务消息如果本地事务执行超时(比如 DB 抖动),半消息会一直挂着,直到超时回查。回查次数要配置合理,不然半消息堆积影响性能。
五、FAQ 快答
Q:为什么不直接用 HTTP 异步回调? HTTP 回调没有重试、没有削峰、没有消息堆积管理。MQ 的价值不只是异步,还有削峰填谷和可靠性保障。高峰期 1 万笔订单的短信、积分、渠道推送,靠 HTTP 回调并发去打下游,下游直接被打垮。
Q:RocketMQ 和 Kafka 到底怎么选? 需要事务消息、团队不熟 Kafka → RocketMQ;追求极致吞吐、消息可以接受一定丢失风险 → Kafka。景区票务对消息可靠性要求高,我们选 RocketMQ 更合适。
Q:异步化后对账怎么做? 异步逻辑最终都要落库,日终对账任务按订单号聚合所有异步结果,对比"订单状态 vs 各环节结果",差异项自动标记人工处理。异步化没有改变对账逻辑,只是把对账数据的产生从同步改成了异步。
Q:小团队用 MQ 会不会过度设计? 单机版业务确实不需要。判断标准:同步链路里有没有"非核心但耗时"的逻辑(短信、通知、积分)?有没有高峰削峰需求?有就用,没有就先别上。
最后说点实在的。消息队列不是银弹,它是把同步链路里的非核心逻辑拆出去的工具 。中科智旅景游宝这次改造最直观的结果:下单接口响应从 2.5 秒降到 400ms,黄金周高峰期短信、渠道推送再没拖过后腿,重复下单问题也少了大半。核心原则就一句话------同步保体验,异步保扩展,可靠性靠三层兜底。
评论区聊聊你们系统的异步化改造,MQ 选型踩过什么坑?消息丢失、重复消费、顺序错乱,哪个最要命?