3.2 异步通信:消息队列与事件驱动


3.2 异步通信:消息队列与事件驱动

摘要:上一节讲完同步,这一节讲它最好的搭档——异步。发送方把消息丢进队列就转身走人,接收方有空再处理。本文讲清消息队列与事件驱动的差别、什么时候该异步、以及解耦的代价:你再也拿不到即时结果,事情没做完得靠"最终一致"兜底。

同步通信问的是"你要不要立刻给你结果",异步通信回答的是"不打扰你,稍后再说"。它的直觉模型就是消息队列:生产者把消息放下,消费者有空就来取。好处肉眼可见——解耦、削峰、容错;代价同样明显——没有即时响应,出错了要你自己追

消息队列:解耦的现场

看一张典型的异步结构:

订单服务"下单成功"这件事发一条消息到队列,库存、通知、风控各自订阅自己关心的处理。生产者不再需要知道"谁会响应我、有几个响应者"——它只负责发布。这就是解耦的精髓:生产者和消费者互不认识,通过队列当中间人。加一个新的消费者,生产者一行都不用改。

别把队列理解成"管道"

一个常被记错的点:队列不是先把消息"运到某处",然后你要去那处取。它是消息的中心暂存 + 派发中枢。消费者拉走一条就少一条,而且通常一条消息只被一个消费者处理(点对点)。这和"主题广播"是两种语义,别混。

事件驱动:不止是"发消息",是"记事实"

消息队列偏"指令式":订单服务告诉库存服务"去扣库存"。事件驱动偏"事实式":订单服务发布"订单已创建"这个已发生的事实,谁关心谁订阅,各自反应。区别在于,指令是对某人的命令,事件是对所有人的声明。

指令式: 库存服务,去扣减库存 事件式: 订单已创建(库存、通知、积分各自响应)

事件驱动的诱人之处在于它天然支持"未来的消费者"——半年后你要加个"数据分析"功能,订阅同样的"订单已创建"即可,老服务完全无感。这也是微服务事件溯源、CQRS 那些做法的地基,我们在第 4 章会用 MQ 配合 Saga 再碰一次。

什么场景"发出去就不管"是对的

和 3.1 的判定是同一把尺子倒过来用——如果下一秒没有结果,这笔交易依然成立,就异步

  • 通知类:下单成功要发邮件、短信、推消息,慢了无碍,同步反而拖垮主链路。
  • 批处理类:耗时的对账、报表生成、数据清洗,放后台慢慢跑。
  • 削峰填谷:秒杀、抢购瞬间请求暴涨,把流量先收进队列,后端按自己能扛的速度慢慢消化,保护数据库不被冲垮。
  • 事件广播:一次变化要通知多个下游,广播比逐个同步调用灵活得多。

拿了异步,就得还一份"最终一致"的账

这是异步最实诚的提醒。同步给了你即时一致,异步只给你最终一致。你在下单时发消息扣库存,但如果消息丢了、消费者崩了、处理一半失败了,用户看到的订单和实际库存可能短时间对不上。异步不等于"丢消息也无所谓",恰恰相反,它要求你主动把"对不上账"的风险兜住。

三段兜底动作,至少占一段:

  1. 消息不丢:落库再发(本地事务表),或配生产者/消费者的确认重投,进死信队列。
  2. 消费幂等:同一个事件被重放两次,结果也要一样(用业务唯一键去重)。
  3. 对账兜底:定期跑"订单 vs 库存"对账任务,把最终一致"拉回"现实。

下面的示意表达"先落库再发"的思路(意为伪代码,非完整可跑实现):

with db.transaction(): db.save(order) # 1. 订单落库 outbox.insert(event=OrderCreated(owner=order_id)) # 2. 动兴事件先入本地待发表 # 3. 后台把 outbox 里的消息投递到 MQ,投递成功再删除 dispatch_outbox()

这里的关键叫 outbox 模式:把"发消息"和"写业务数据"放进同一个本地事务,保证不会出现"业务改成功但消息没发出去"的半吊子。否则你会在线上反复看到"库存扣了但订单没通知"这类脏账。推迟的正确性依赖,就在这个细节里。

把这条链画成时序,能看清"业务与消息的原子性"落在哪一步:

outbox 时序:先落库再兜底送消息

outbox 时序:先落库再兜底送消息

取舍:别把异步当万能解药

异步解耦,但代价是:调试变难(没有直观的调用栈,出错要顺着消息链路找)、延迟变高(消费者不一定立刻在)、一致性复杂(要最终一致方案)。所以正解是混合:主链路强一致性环节走同步,可缓的、广播的、削峰的环境走异步。真把所有服务全异步化,你会得到一个"什么都对不上账、出事查不清"的旋涡,那比单体还难养。

别忘了消息的"顺序"这个隐形成本

异步的账里还有一条经常被欠着没还:消息的顺序。同步调用里,调用顺序就是处理顺序,天然可靠;可一旦上了队列,你随手发出的"下单""取消下单""修改收货地址"三条消息,被投递、被消费的顺序未必和发出时一致。若消费方恰好靠"最后一条生效",顺序一乱,用户的地址可能被改回旧的,甚至一次退款被先于下单处理。

应对就两招,选对场景再上:一是让同一种业务的同一条消息走同一个分区(partition),消费者在分区内保序——只要保证同一单的发地址一路排好队就行;二是消费方不做"顺序依赖",把每条事件都设计成"独立性足够、不care谁先谁后"。前者是基础设施的事,后者是事件建模的事。多数团队踩的坑,都是在设计事件时没考虑"对方收件顺序可能和我说的不一样",等到乱序捅了娄子才补课。

本节要点

  • 一句话兜底:同步要即时结果,异步只给最终一致,别在两者都期待"轻轻松松"

  • 队列是中心暂存+派发中枢,点对点消费;事件是"事实声明",谁订阅谁响应

  • 下一秒没有结果交易仍成立→异步;需要即时结果→同步

  • 异步的回报是解耦削峰,代价是只给你最终一致

  • 用"先落库再发(outbox)"“消费幂等”“对账兜底”把账拉回来

  • 别全异步化,混合同步与异步才是常态


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U