什么时候真的需要消息队列
订单服务创建完订单,要通知三个下游:库存预占、积分发放、短信提醒。云上的 SaaS 版第一版写法是订单事务提交后,串着调三个 HTTP 接口,积分服务一慢,订单创建接口跟着超时,客户在收银台前干等。开发组把 RocketMQ 请了进来,问题解决,三个月后这个系统卖私有化,客户的内网环境只有 MySQL 和两台应用服务器,运维就一个人,RocketMQ 一套 NameServer 加 Broker 架构(NameServer 是注册中心,Broker 是存储转发节点,RocketMQ 最少这两类进程)装是装上了,客户问了一句:这个坏了我们谁来修?
这篇按「B 端中小量级」写;日均投递远超百万级的系统不在这个范围。
先给答案:要不要消息队列(MQ,Message Queue,生产者把消息投进队列、消费者各自取走的中间件),取决于三个问题的答案——这条数据丢不起吗?能晚多久?一条变更要通知几方?三个答案都不紧迫的系统,一张数据库任务表就能把事情做对,而且它在客户环境里是零新增组件。
丢不起、等不起、还是通知不止一方
三个问题对应三种真实需求,一个一个过:
- 丢不起:订单要扣库存、支付要发积分,这类指令丢了就是资损或者客诉。反过来,埋点上报、日志采集丢了就丢了,不值得为它们上可靠投递设施。很多人把第二类也搬进 MQ,等于花可靠的成本干不可靠的事。
- 等不起:判定标准落在业务上——异步之后这件事还成立吗。下单后十分钟发货可以异步,收银台等支付结果不能异步。注意「能晚多久」的单位经常是分钟不是毫秒——多数 B 端系统的异步需求按分钟计就够了,这一条直接决定后面选型。
- 通知几方:一个订单创建,库存要听、积分要听、风控也要听,而且日后还会冒出第四个第五个消费者。这种「一条事件、多方订阅、消费者会持续增加」的形状下,MQ 的解耦价值才真正兑现——生产者不再认识任何一个消费者。如果从头到尾只有一个消费者,解耦是纸面上的:你不过把 HTTP 调用换成了 MQ 调用,多养了一套中间件。
对着三个问题把常见场景过一遍:
| 场景 | 丢 | 晚 | 几方 | 判定 |
|---|---|---|---|---|
| 支付成功发短信 | 丢得起(可重发) | 分钟级 | 1 方 | 任务表 |
| 订单创建通知库存预占 | 丢不起 | 秒到分钟级 | 1–2 方 | 任务表起步,量大迁 MQ |
| 秒杀流量削峰 | 丢不起 | 秒级 | 1 方,但瞬时量大 | MQ |
| 一条事件喂报表、风控、推荐多个团队 | 看情况 | 分钟级 | 3 方以上且在增长 | MQ |
| 埋点、日志收集 | 丢得起 | 小时级 | 1 方 | 哪个都别用,写文件或直接丢 |
削峰单独说一句:MQ 在这里的作用不是可靠投递而是蓄水池,瞬时写入先落队列,消费者按自己的节奏消化。这个场景没有低配替代——任务表扛不住秒级上万写入,单表写入吞吐先到顶,库存行的行锁也挤成热点。

一张任务表能顶很久
任务表的原理一行话:把「要发的消息」当成本地事务的一行数据写进数据库,提交事务的同时消息就落库了,另一个定时任务扫表往外发,发成功才改状态。这个模式有个名字叫事务发件箱(Transactional Outbox,本地事务和消息发送的原子性靠同一个数据库事务保证),它补的是 MQ 管不到的一段缝隙——业务事务已经提交,消息还没发出去,或者反过来;MQ 保证的是发出之后不丢(前提是同步刷盘加同步复制都配上,默认的异步刷盘在 Broker 宕机时会丢,私有化客户机器少的环境尤其要核对),管不到发出之前。表结构照这个写:
CREATE TABLE outbox_task (
id BIGINT NOT NULL PRIMARY KEY,
biz_type VARCHAR(32) NOT NULL, -- order_created / stock_deduct
biz_id BIGINT NOT NULL,
payload TEXT NOT NULL, -- 消费者要的完整数据,JSON
status VARCHAR(16) NOT NULL DEFAULT 'PENDING',
owner VARCHAR(64) NULL, -- 抢到这行的实例标识
next_retry DATETIME NOT NULL, -- 不晚于它的行才会被扫到
retry_count INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
KEY idx_scan (status, next_retry)
);
业务代码里只有一个动作:在同一个事务里往 outbox_task insert 这一行,biz_type 加 biz_id 标消息从哪来,排障时靠它们回溯业务单据。payload 用 TEXT 别用 VARCHAR 定长——多行商品的订单 JSON 轻松超过 2000 字符,而 insert 在订单事务里,超长报错等于把下单一起拖垮。
扫表任务每 5 秒跑一次(我的经验值,按消费者承受力调),单批 200 行:先用条件更新抢占,UPDATE outbox_task SET status='SENDING', owner=? WHERE status='PENDING' AND next_retry<=NOW() LIMIT 200,再查回 owner 是自己的行逐条发送,成功置 SENT,失败把 retry_count 加一、按指数退避推 next_retry(第 n 次失败后 2 的 n 次方分钟再试),重试上限 8 次(同样是校准值)标 DEAD 等人看。SENDING 这个中间态要配超时回收:实例发送中途宕机,行会卡在 SENDING,回收任务把 owner 是自己且超过 10 分钟没动静的行重置回 PENDING。
有三个容易忽略的细节:
- 完整数据:payload 存消费者要的完整数据,别存「事件类型让消费者回来查库」——查库又把两个系统在数据库层耦上了。
- 联合索引:idx_scan 是 (status, next_retry) 联合索引,扫描走「status 定前缀、next_retry 定范围」的路径,发完的行变成 SENT 后自然退出扫描范围,这才是联合索引真正的收益,单列 status 在表大了之后扫起来全是无效行。
- 死信出口:DEAD 状态的行要有人看的出口,接告警或管理页,否则它的名字叫死信但没人知道(处置细节在死信那一篇短文里)。
还有一句实话必须说在前面:任务表和 MQ 一样是至少一次投递(at-least-once,置 SENT 前宕机、超时回收、多实例抢占,任何一个环节都可能让同一条消息发两遍),消费者的防重一道都不少做——选任务表省下的是中间件的运维账,不是消费者幂等的功课。
任务表的真实容量比想象的大:消费者响应在几十毫秒级、单批 200 行的配置下,每秒几十条、每天百万级的投递量,一个扫表循环加两三个 worker 实例绰绰有余。它真正的天花板有三个:
- 写入热点:所有业务事务都在同一张表上 insert,高并发时瓶颈在单表写入吞吐本身——redo 日志、自增主键的页争用、连接数,不在行锁,insert 之间本来就不冲突。
- 扫描竞争:多实例抢同一批行靠上面的 SENDING 加 owner 协议,抢得太细、批次切太小反而锁冲突多。
- 表膨胀:每天百万级一年就是三亿多行,SENT 的行要定期归档或清理,这个任务别忘了排。
三个天花板都碰到之前,私有化和中小 SaaS 的绝大多数异步需求到不了量。
装上之后的三笔新账
决定上 MQ,钱不是许可费,是三笔持续支出:
- 堆积:消费能力跟不上生产速度,消息在队列里排长队,MQ 自己扛得住亿级堆积,业务的时效性扛不住——积分晚发一小时没人发现,库存预占晚一小时就是超卖,因为这一个小时里货没有被占住,系统当它有货,接着卖就超了。
- 重复消费:MQ 的投递语义是至少一次(at-least-once,网络抖动时同一条消息可能投两次以上),消费者的每个接口都得自己防重,这在支付回调那篇写过一遍,MQ 把同样的功课又布置了一次,而且这一次没有现成的机制替你把关。
- 顺序:同一张订单的「创建」「支付」「退款」三条消息如果被两个消费者并行处理,库存可能先收到退款再收到创建。RocketMQ 并发消费模式下失败重试默认 16 次,重试消息转进重试主题按延迟等级重投、不堵同队列后面的消息,16 次耗尽进死信;顺序消费才是堵着后面等的那个,而且它的原地重试上限要显式配置,不配就一直在原地转。两种模式的坑形状不同,选哪个之前想清楚自己要哪种(重试与死信的处置细节,后面重复消费和死信两篇短文接住)。
三笔账加起来,就是「客户问坏了谁来修」的完整答案:监控要看堆积深度,版本要跟着升,重试和死信要有人值班。这套运维成本私有化客户全额承担,云上 SaaS 则由平台团队摊薄——同一个技术选型,两种交付形态下答案可以不同,这不叫双标,叫成本归属不同。
动手前把这张表对一遍
四条判据:
- 三项全占:丢不起且晚不起且多方订阅,直接上 MQ,别用任务表过渡到一半再迁——表到 MQ 的迁移不难,难的是迁移时消费者已经写了三套。
- 只占一项:只丢不起或只多方订阅,任务表起步,把 outbox 的表结构和扫表逻辑写成通用组件,迁 MQ 时业务代码只换投递实现。迁移的触发信号看两个数:outbox 写入占到业务库写 TPS 的一成以上,或者消费者涨到三家以上。
- 削峰广播:纯粹削峰或广播,任务表没有替代方案,上 MQ。
- 都无所谓:三个问题都答「无所谓」的,把需求退回去再问一遍,大概率它连任务表都不需要,同步调用加个超时就完了。
MQ 是工具不是勋章,三个问题的答案比任何选型文章可靠。
系列:相关 支付回调:幂等、重试与对账 | 私有化部署的最小交付包 | 技术纵深系列索引