先交代一起事故
上一篇结尾提过:老王把 MQ 插进下单链路的第三周,会员王阿姨的积分翻了一倍。排查下来原因很简单——积分服务消费超时,MQ 没等到 ACK,按规则重投了一次。两次消费,两份积分,客服电话响了一上午。
这不是 MQ 的 bug,而是它的设计使然。这一篇把引入 MQ 要背的债一条条摆上桌——先把代价看清楚,再决定吃不吃这顿午餐。
代价一:一致性从数据库问题变成了分布式问题
同步调用时代,订单和积分库哪怕是两个库,下单服务也能在业务上保证先后:先扣库存成功,再发积分,失败了就补偿。调用方看得见每一步的结果。
改成消息之后,事情劈成了两半:
- 订单落库成功,消息没发出去——下游永远不知道有这单,积分丢了一份。这是「发消息」和「写数据库」两步之间的原子性问题;
- 消息发出去了,订单落库失败——下游白忙一场,凭空多了笔积分。
数据库事务的边界只到自己那台库,伸不进 MQ,也伸不进下游的库。原子性要靠新的机制去补:本地消息表、事务消息、对账补偿——这些是第 10 篇的主菜,这里先立个碑:拆开同步的那一刀,同时切断了事务的边界。
代价二:重复消费是常态,不是意外
王阿姨的积分翻倍不是倒霉,是必然。MQ 的可靠性承诺叫「至少一次投递」(At Least Once):消息宁可重发一千次,也不允许悄悄丢掉。为什么这么设计?因为发送方和 Broker 之间的确认可能丢——消息明明送到了,ACK 却在网络里迷了路,发送方只能重发。想做到「恰好一次」,要么用两阶段提交把性能拖垮,要么就得把幂等做进业务里。
所以消费端的铁律是:每一条消息都可能被投递多次,消费逻辑必须幂等。加积分这种操作要带业务单号去重,而不是裸 add。幂等的具体三板斧(唯一键、状态机、去重表)在第 7 篇展开。
代价三:一条消息的一生,横跨五个系统
同步调用排障:看调用链,A 慢就是 A 的问题,一步到位。消息链路排障:消息从生产者发出,进 Broker 存储,路由到队列,被消费者拉走,消费失败进重试,重试耗尽进死信——每一跳都可能出事,每一跳的日志在不同的系统里。
老王后来复盘积分事故时,光是把「消息到底投递了几次」查清楚就花了俩小时:生产端日志在订单服务,投递次数在 MQ 控制台,消费日志在积分服务,三者还对不上时间戳。引入 MQ 的那一刻,就必须同步上链路追踪:给每条消息带上全局唯一的 messageKey 和 traceId,否则出事时你连案发现场都拼不出来。
代价四:运维是一份新工作
MQ 自己就是一个分布式系统:要部署、要扩容、要监控、要升级、要做磁盘容量规划。队列积压要有人看,磁盘打满会导致 Broker 拒写,版本升级可能触发协议变更。小团队上 MQ 之前先掂量:有没有人能接住它出事的时候。用云厂商的托管服务可以省掉一部分,但监控告警、积压处理、容量规划这些事省不掉。
代价五:延迟变得不确定
同步调用的延迟是可预期的:下游 100ms,那就是 100ms。消息链路的延迟取决于队列深度:空闲时毫秒级,积压十万条时可能要等几分钟。异步的延迟是「尽力而为」,不是「承诺兑现」。业务上对时效敏感的路径(比如库存扣减),别指望 MQ 给你确定性。
哪些场景不该用 MQ
| 场景特征 | 为什么不该 |
|---|---|
| 强一致要求:扣款、扣库存 | 异步意味着窗口期不一致,超卖就是这么来的 |
| 调用链短且量小 | 两个服务每天百来条消息,引入 MQ 纯属给自己找事 |
| 调用方需要立即拿到结果 | MQ 不返回业务结果,查询类请求老老实实走 RPC |
| 没人维护 MQ | 引入一个没人接得住的中间件,等于埋雷 |
上 MQ 之前,先问四个问题
- 这条支路允许延迟和最终一致吗?不允许,就留在同步链路里;
- 消费逻辑能改成幂等吗?不能改,先别上队列,先改代码;
- 出问题时有链路追踪和对账手段吗?没有,先把可观测性补齐;
- 谁值班谁会修?答不上来,先培训或者先别上。
四个问题都答得上来,这顿午餐才吃得踏实。
小结
MQ 的代价清单:一致性要重新设计、重复消费必须幂等、排查链路变长、运维多摊子事、延迟不再确定。它换来的解耦、异步、削峰都是真金白银,但先掂量债务,再谈收益。
代价认清了,下一个问题是:市面上 RocketMQ、Kafka、RabbitMQ 三大主流各有性格,哪一台适合老王的咖啡馆?下一篇做选型,把三家的账摊开来算。
评论 (0)