摘要:上一节讲完同步,这一节讲它最好的搭档——异步。发送方把消息丢进队列就转身走人,接收方有空再处理。本文讲清消息队列与事件驱动的差别、什么时候该异步、以及解耦的代价:你再也拿不到即时结果,事情没做完得靠"最终一致"兜底。
同步通信问的是"你要不要立刻给你结果",异步通信回答的是"不打扰你,稍后再说"。它的直觉模型就是消息队列:生产者把消息放下,消费者有空就来取。好处肉眼可见——解耦、削峰、容错;代价同样明显——没有即时响应,出错了要你自己追。
看一张典型的异步结构:
订单服务"下单成功"这件事发一条消息到队列,库存、通知、风控各自订阅自己关心的处理。生产者不再需要知道"谁会响应我、有几个响应者"——它只负责发布。这就是解耦的精髓:生产者和消费者互不认识,通过队列当中间人。加一个新的消费者,生产者一行都不用改。
一个常被记错的点:队列不是先把消息"运到某处",然后你要去那处取。它是消息的中心暂存 + 派发中枢。消费者拉走一条就少一条,而且通常一条消息只被一个消费者处理(点对点)。这和"主题广播"是两种语义,别混。
消息队列偏"指令式":订单服务告诉库存服务"去扣库存"。事件驱动偏"事实式":订单服务发布"订单已创建"这个已发生的事实,谁关心谁订阅,各自反应。区别在于,指令是对某人的命令,事件是对所有人的声明。
指令式: 库存服务,去扣减库存 事件式: 订单已创建(库存、通知、积分各自响应)
事件驱动的诱人之处在于它天然支持"未来的消费者"——半年后你要加个"数据分析"功能,订阅同样的"订单已创建"即可,老服务完全无感。这也是微服务事件溯源、CQRS 那些做法的地基,我们在第 4 章会用 MQ 配合 Saga 再碰一次。
和 3.1 的判定是同一把尺子倒过来用——如果下一秒没有结果,这笔交易依然成立,就异步:
这是异步最实诚的提醒。同步给了你即时一致,异步只给你最终一致。你在下单时发消息扣库存,但如果消息丢了、消费者崩了、处理一半失败了,用户看到的订单和实际库存可能短时间对不上。异步不等于"丢消息也无所谓",恰恰相反,它要求你主动把"对不上账"的风险兜住。
三段兜底动作,至少占一段:
下面的示意表达"先落库再发"的思路(意为伪代码,非完整可跑实现):
with db.transaction(): db.save(order) # 1. 订单落库 outbox.insert(event=OrderCreated(owner=order_id)) # 2. 动兴事件先入本地待发表 # 3. 后台把 outbox 里的消息投递到 MQ,投递成功再删除 dispatch_outbox()
这里的关键叫 outbox 模式:把"发消息"和"写业务数据"放进同一个本地事务,保证不会出现"业务改成功但消息没发出去"的半吊子。否则你会在线上反复看到"库存扣了但订单没通知"这类脏账。推迟的正确性依赖,就在这个细节里。
把这条链画成时序,能看清"业务与消息的原子性"落在哪一步:

异步解耦,但代价是:调试变难(没有直观的调用栈,出错要顺着消息链路找)、延迟变高(消费者不一定立刻在)、一致性复杂(要最终一致方案)。所以正解是混合:主链路强一致性环节走同步,可缓的、广播的、削峰的环境走异步。真把所有服务全异步化,你会得到一个"什么都对不上账、出事查不清"的旋涡,那比单体还难养。
异步的账里还有一条经常被欠着没还:消息的顺序。同步调用里,调用顺序就是处理顺序,天然可靠;可一旦上了队列,你随手发出的"下单""取消下单""修改收货地址"三条消息,被投递、被消费的顺序未必和发出时一致。若消费方恰好靠"最后一条生效",顺序一乱,用户的地址可能被改回旧的,甚至一次退款被先于下单处理。
应对就两招,选对场景再上:一是让同一种业务的同一条消息走同一个分区(partition),消费者在分区内保序——只要保证同一单的发地址一路排好队就行;二是消费方不做"顺序依赖",把每条事件都设计成"独立性足够、不care谁先谁后"。前者是基础设施的事,后者是事件建模的事。多数团队踩的坑,都是在设计事件时没考虑"对方收件顺序可能和我说的不一样",等到乱序捅了娄子才补课。
一句话兜底:同步要即时结果,异步只给最终一致,别在两者都期待"轻轻松松"
队列是中心暂存+派发中枢,点对点消费;事件是"事实声明",谁订阅谁响应
下一秒没有结果交易仍成立→异步;需要即时结果→同步
异步的回报是解耦削峰,代价是只给你最终一致
用"先落库再发(outbox)"“消费幂等”“对账兜底”把账拉回来
别全异步化,混合同步与异步才是常态