速成课 · No. 18
当系统的一部分需要另一部分做点事情时,简单的做法是调用它并等待答复。强大的做法是把一条 message 丢进 queue 里然后继续往前走,让对方在准备好的时候再来取。这一个转变——从调用并等待,变成发消息并继续——正是系统保持快速、在故障中存活并实现扩展的方式。
21 个概念 · 每个都有一张图 · 结尾有速查表
要明白 queue 为什么存在,先得切身体会一下那个简单替代方案的痛:一个服务调用另一个服务并等待它。它一直管用,直到真正要紧的那一刻。
地图:六组共 21 个概念
一张地铁图:先看清整条线路,再一站一站地坐过去。
先讲问题——为什么直接调用会让你失望。然后每组一章:queue(想法本身)、角色(谁写、谁读、谁保管)、command 与 event(两种消息)、好处(你付出代价换来了什么)和难题(什么会出问题)。最后一章讲什么时候该用 queue、什么时候不该用,另附关系图和速查表。
如何阅读每个概念
一本护照:每个人的栏目都一样,所以很容易比较。
从这里开始,每个概念都是同一个结构:它的英文名称(记住它——这是你会在 broker 文档和面试里遇到的词)、一幅日常生活的画面、一张图,以及一张卡片:问题、思路、例子、代价,以及它与其他概念的关联。最后是关系图和速查表。
Synchronous Call——一次直接调用把两个服务绑在一起
给某人打电话,然后一直占着线直到他们把事情做完——你不能挂断,不能做别的任何事,而如果对方不接,你就被卡住了。
服务 A 让服务 B 做点事,最显而易见的办法是 synchronous call(同步调用):A 发问,然后等待,一直扣着手上的一切,直到 B 回答。这把它们紧紧地耦合在一起——A 的速度被 B 的速度绑架,B 没做完 A 就没法继续。如果只是快速答复,这没问题。但对缓慢或繁重的工作,A 的时间都花在了原地冻结、等别人上。
- 问题
- 调用方被冻结,直到被调用方回答。
- 思路
- 发问、等答复、再继续。最简单的交谈方式。
- 例子
- 订单服务通过 HTTP 调用价格服务,等拿到价格后才显示购物车。
- 代价
- 紧密的耦合:A 的速度和可用性取决于 B。
- 关联
- 它的痛点由 Message Queue 来治;需要即时答复时,它仍是最佳选择。
Failure Propagation——被调用方挂了,调用方也跟着坏
打电话给仓库确认订单,因为没人接,你就拒绝接收任何新订单——一扇关上的门,堵死了整家店。
更深的问题是故障。如果 A 调用时 B 挂了或过载了,A 的请求也会失败——故障沿着调用链一路向上传播。一个缓慢或损坏的服务就能让所有依赖它的东西停摆,流向 B 的流量激增也会把 A 一起拖垮。紧密耦合意味着弱点会蔓延,系统的可用性只取决于它最脆弱的那一环。
- 问题
- 一个服务出故障,会变成所有调用方都出故障。
- 思路
- 同步调用链的可用性,只取决于最弱的那一环。
- 例子
- 仓库不响应 → 订单报错 → 结账不能用了。
- 代价
- 流向一个服务的流量激增,会拖垮它上面的一切。
- 关联
- 用缓冲来治:Message Queue 带来 Resilience——故障变成延迟。
Background Work——有些工作不该让用户等
在收银台,你付了钱,拿着小票就走——不会站在柜台前看他们打包、发货、给你发邮件。慢的部分在你走后才进行。
很多工作不需要在用户收到响应之前完成——发确认邮件、生成发票、缩放照片。这就是 background work(后台工作)。让用户等这些全部做完,既慢又没必要。你想要的是接下请求、快速答复,再让慢的部分随后进行。直接调用做不到这一点;它让所有人为所有事情等待。
- 问题
- 用户在等一些其实与他们的响应无关的工作。
- 思路
- 接下请求,快速答复,慢的部分之后再做。
- 例子
- 付款成功——马上给小票;邮件、发票和照片缩放放到后台。
- 代价
- 后台工作的结果不会立刻出现。
- 关联
- 用 Message Queue 和 Asynchronous Processing 来实现。
直接调用把两个服务在时间上耦合起来:调用方要等,还要承受被调用方的缓慢和故障。对于缓慢或不紧急的工作,这就是个陷阱。
解决办法是别再调用,改为发消息。queue 是中间的缓冲,让一边留下工作、另一边来取——在时间上把两者解耦。
Message Queue——丢下消息,继续往前走
留一条语音留言,而不是在电话那头干等:说清你要什么,挂断,继续过你的一天——他们有空时自会处理。
A 不再调用 B 并等待,而是写一条描述工作的 message(消息),把它丢进 queue——一条让消息排队等待处理的队伍。A 立刻就自由了,可以继续;它不在乎 B 什么时候处理。这条消息是一个小包裹,写着“需要做的事”,是被交接出去,而不是当场执行。这次交接就是全部的想法。
- 问题
- 直接调用让发送方一直占着线。
- 思路
- 把工作描述成一条 message 放进 queue;接收方自己来取。
- 例子
- 不去调用邮件服务——而是在 queue 里放一条带订单号的
SendEmail消息。 - 代价
- 多了一个需要运行和理解的活动部件。
- 关联
- 带来 Temporal Decoupling;由 Broker 保管;由 Consumer 读取。
Temporal Decoupling——queue 是时间上的缓冲
办公桌上的收件盘:工作堆在里面,被平稳地处理,所以突然涌来的一堆文件不会压垮那个人——只是让盘子暂时变高了一点。
queue 夹在两边之间,作为缓冲,在消息被处理之前先保管着。这就在时间上把它们解耦(temporal decoupling):发送方和接收方不再需要同时保持快速、可用,甚至不需要同时在运行。突发产生的工作可以被平稳地消费。queue 吸收了工作到来的速度与能完成的速度之间的差距。
- 问题
- 双方必须在同一时刻都活着、都够快。
- 思路
- 在它们之间放一个缓冲:发送方和接收方各按各的节奏工作。
- 例子
- 一阵突发的订单堆在 queue 里,以稳定的速度被处理。
- 代价
- 工作可能要在 queue 里等——结果不是即时的。
- 关联
- Resilience 和 Load Leveling 的基础。
Asynchronous Processing——不等结果
寄一封信,而不是当面交谈——寄出后就继续忙自己的,相信对方之后会读到并处理,不用你站在旁边。
这就是异步工作:发送方不等结果。它把消息发出去就继续,结果在之后、在带外发生。你放弃了直接调用那种即时答复,换来的是不再被阻塞。对于任何不需要立刻答复的事,这笔交易是快速、有韧性的系统的基础。
- 问题
- 在等一个你此刻并不需要的结果。
- 思路
- 发出消息,继续往前;结果在之后、在带外发生。
- 例子
- 报告在后台生成;准备好后用户再拿到。
- 代价
- 没有即时答复;更难追踪发生了什么。
- 关联
- Synchronous Call 的反面;会带来 Eventual Consistency。
queue 是发送方与接收方之间的缓冲。丢下一条消息就继续走——工作在时间上被解耦,等对方准备好时再异步完成。
三种角色让 queue 运转起来,给它们起好名字,就能理清大部分行话。一边制造消息,一边处理消息,中间有个东西把消息稳妥地保管起来。
Producer / Consumer——一边制造工作,一边完成工作
一间厨房:服务员把点菜单钉在架子上,厨师一张张取下来做。两边从不直接说话——那排单子架就是全部接口。
创建消息的一方是 producer(或 publisher);处理消息的一方是 consumer(或 worker)。producer 丢下一张单子;consumer 取走一张,做完活,再取下一张。它们互相不认识也不互相等待——只认识 queue。这种干净的分离,让每一边都能各自构建、扩展和修改。
- 是什么
- producer 创建消息;consumer 取走消息并完成工作。
- 思路
- 双方只认识 queue,不认识彼此。
- 例子
- 网页应用丢下“缩放这张照片”;worker 取走消息并完成缩放。
- 陷阱
- producer 不知道工作何时、由谁来完成。
- 关联
Broker 位于两者之间;一个 queue 上有很多 consumer,就是 Competing Consumers。
Message Broker——broker 稳妥地保管消息
寄件人和收件人之间的邮局:它收下你的信,妥善保管,直到收件人来取——所以即使对方不在,也不会丢。
中间是 message broker——像 RabbitMQ 或 Apache Kafka 这样的软件,负责接收消息、可靠地存储,并把它们交给 consumer。它就是系统的邮局。可靠性是关键:即使所有 consumer 都挂了,broker 也会把消息保管好,直到有人准备处理,所以工作是被搁置,而不是丢失。
- 问题
- 消息需要一个安全的地方存放,直到有人取走。
- 思路
- 专门的软件接收、存储并投递消息——系统的邮局。
- 例子
- RabbitMQ、Apache Kafka。
- 代价
- 又多一个需要运行和维护的系统。
- 关联
- 保管 Message Queue;Resilience 建立在它的可靠性之上。
Competing Consumers——加 consumer 就能更快
点菜单堆起来时,你就往流水线上多加几个厨师——每人取下一张单子,积压清得更快,而订单进来的方式不用改。
因为 consumer 只是取下一条消息,所以你可以让很多 consumer 并行地消费同一个 queue——这就是 Competing Consumers 模式。积压在增长?多加几个 worker,它们会自动分担负载——每个抓取不同的消息。这是 queue 的一大优势:你可以通过增加 consumer 独立地扩展慢的那一部分,而不用碰 producer 或 queue 本身。
- 问题
- 一个 worker 跟不上;积压越来越多。
- 思路
- 多个 consumer 读同一个 queue;每条消息只交给其中一个。
- 例子
- 大促之前——照片缩放 worker 从两个加到十个。
- 代价
- 并行处理会打乱顺序——见 Message Ordering。
- 关联
- Independent Scaling 的基础;适用于 Command 消息。
producer 创建消息,consumer 处理消息,broker 在中间稳妥保管。需要更高吞吐?加 consumer——它们自动分担这个 queue。
一条消息可能是两种截然不同的东西:一道要求做某事的命令,或一则某事已经发生的通告。第二种会悄悄改变你设计整个系统的方式。
Command——command 告诉一个 worker 去做一项任务
交给某个特定部门的一张工单:“缩放这张图片。”它指明任务,期望恰好一个团队去做,然后就结束了。
command 消息是指向一个 consumer 的直接指令:“发这封邮件”“处理这笔付款”。这是把 queue 当作待办清单来用——producer 知道该发生什么,把任务交出去,由取走它的那个来做一次。这是 queue 最简单的用法:把具体的工作卸出去,稍后恰好做一次。
- 是什么
- 一条命令式的消息:“做这个”。用动词命名。
- 如何运作
- producer 知道该发生什么;由一个 consumer 完成任务。
- 例子
SendEmail、ProcessPayment、ResizeImage。- 陷阱
- producer 仍然知道需要什么工作——还留有一些耦合。
- 关联
- Event 的反面;常由 Competing Consumers 来处理。
Event——event 宣告某件事已经发生
布告栏上的全公司通告:“刚刚有一位新客户注册了。”它不告诉任何人该做什么——谁关心,谁就按自己的方式反应。
event(事件)不一样:它陈述一个关于过去的事实——“订单已下”“用户已注册”——而不说谁该做什么。producer 只是宣告它,不知道也不关心谁在听。也许没人反应;也许有五个服务反应。这颠倒了关系:你不再命令某个特定的 worker,而是广播某件事成立了,让感兴趣的各方自己决定。
- 是什么
- 关于过去的事实:“这件事发生了”。用过去时命名。
- 如何运作
- producer 宣告它,不知道谁在听:零个、一个或五个服务。
- 例子
OrderPlaced、UserRegistered。- 陷阱
- 更难看清最终会发生什么——反应分散在各个服务里。
- 关联
- Command 的反面;通过 Pub/Sub 投递;Event-driven 的基础。
Publish/Subscribe——一个 event,多种反应
一份报纸只印一次,送到每位订阅者手里,每个人出于各自的原因阅读——体育迷、投资人、填字游戏爱好者——都来自同一期。
这种广播模型就是 publish/subscribe(pub/sub,发布/订阅):producer 把 event 发布到一个 topic,每个感兴趣的 consumer 订阅后拿到自己的一份副本。一个“订单已下”的 event 可以同时触发邮件服务、分析服务和发货服务——这就是 fan-out(扇出)。发布者甚至不知道它们的存在。以后可以随时加新的反应,完全不用动发布者。
- 问题
- 一个 event 对很多服务都重要,而且名单还在增长。
- 思路
- 发布到一个 topic;每个订阅者拿到自己的副本(fan-out)。
- 例子
- “订单已下”→ 邮件、分析、发货同时做出反应。
- 代价
- 每份副本各自单独处理;整条反应链很难看清。
- 关联
- 投递 Event;Event-driven Architecture 建立在它之上。
Event-driven Architecture——event 解开了“谁认识谁”的耦合
给报纸增加一位新订阅者,丝毫不会改变报纸的写法——发布者根本不需要知道谁在读。
这就是 event-driven(事件驱动)设计的深层力量:event 的 producer 与所有对它做出反应的一方完全解耦。想加一个响应“订单已下”的新功能,只需订阅一个新的 consumer——订单服务不用改。这样构建的系统靠增加监听者来成长,而不是靠修改被监听的对象。大型系统就是这样演进的,而不至于所有东西互相依赖。
- 问题
- 每个新功能都意味着要去修改“什么都在那里发生”的那个服务。
- 思路
- 服务宣告 event;功能作为新的监听者加入。
- 例子
- 按订单发放积分——让一个新服务订阅“订单已下”。
- 代价
- Eventual Consistency,而且在消息之间跳转的请求更难调试。
- 关联
- 建立在 Event 和 Pub/Sub 之上;带来 Independent Scaling。
command 告诉一个 consumer 去做一项任务。event 宣告一个事实,让任何人都能反应——而 pub/sub 把一个 event 扇出给许多方,解开了“谁认识谁”的耦合。
queue 增加了活动部件,所以它必须物有所值。它确实做到了,因为它换来了三样别处很难得到的东西:韧性、平稳应对流量高峰,以及独立扩展。
Resilience——consumer 挂了,只是工作被延迟
如果厨房关门一小时,点菜单就在架子上等着——重新开门后,厨师把积压的单子逐一做完。没有一张订单丢失。
因为 broker 保管着消息,consumer 崩溃不会丢工作——消息会等到它回来,然后被处理。对比直接调用,服务挂了就是彻底失败。queue 把“服务挂了”从硬错误变成了暂时的延迟。这种韧性(resilience)——工作经受住故障并恢复——往往是团队选用 queue 的最大单一理由。
- 问题
- 用直接调用时,服务挂了意味着所有人都报错。
- 思路
- broker 保管消息;故障变成延迟。
- 例子
- 邮件 worker 崩溃一小时——邮件在 queue 里等着,重启后发出。
- 代价
- consumer 挂着的时候,工作会堆积并延迟完成。
- 关联
- 治愈 Failure Propagation;依赖 Broker 的可靠性。
Load Leveling——吸收流量高峰
山洪与小镇之间的一座水库:洪峰涌入水库,水库再以稳定、安全的速度向下游放水。
当一大波请求同时涌来——一场大促、一次病毒式传播——queue 让你可以削峰填谷(level the load):高峰填满 queue,consumer 按自己稳定的节奏把它排空。下游缓慢的系统从来看不到高峰,只看到可控、均匀的流量。处理积压所带来的压力叫做 backpressure(背压)。没有 queue,同样的高峰会直接冲击系统,很可能把它冲垮。
- 问题
- 流量高峰直接打到缓慢的系统上,把它冲垮。
- 思路
- 高峰填满 queue;consumer 按自己的节奏处理。
- 例子
- 一场大促:每分钟成千上万的订单,被当作平稳的流量来处理。
- 代价
- 积压增长,工作延迟完成(backpressure)——要盯着 queue 的长度。
- 关联
建立在 Temporal Decoupling 之上;Competing Consumers 负责清掉积压。
Independent Scaling——每一边各自扩展、各自演进
你可以多雇厨师、换厨房设备、再加一个工位——这一切都不用改变服务员点单的方式,因为他们只接触那排单子架。
解耦意味着每一边都可以独立地变化(independent scaling)。按需求增减 consumer;用另一种语言重写某个 consumer;新增一个对现有 event 做出反应的全新 consumer——全都不用碰 producer。queue 是系统各部分之间一道稳定的接缝,而稳定的接缝正是让大系统能一块一块地成长和变化、而不是一次性全部改动的原因。
- 问题
- 改一个部分,就得动所有部分。
- 思路
- queue 是稳定的接缝;每一边各自变化。
- 例子
- 一个 worker 被改用另一种语言重写——producer 根本没察觉。
- 代价
- 消息格式变成了一份契约——修改要谨慎。
- 关联
- 由 Competing Consumers 和 Event-driven Architecture 实现。
queue 换来韧性(工作经得起崩溃)、削峰(高峰被吸收)和独立扩展——这些都是直接调用根本给不了你的好处。
异步消息解决了真实的问题,也制造了新的问题。没有一个是致命的,但假装它们不存在,正是基于 queue 的系统出现隐蔽、令人抓狂的 bug 的原因。
At-least-once 与 Idempotency——消息可能到达两次
为保险起见,邮局有时会再投递一份它不确定是否送到的信的副本——宁可两次也别没有,但现在你可能会对同一条指示执行两次。
大多数 broker 保证 at-least-once(至少一次)投递:它们宁可把消息发两次,也不冒丢失的风险,所以 consumer 偶尔会收到同一条消息不止一次。如果“向客户扣款”执行了两次,那就是真问题。解决办法是 idempotency(幂等):把工作设计成做两次与做一次效果相同(先检查“是否已经扣过款?”)。假定会有重复;让重复变得无害。
- 问题
- At-least-once 投递:同一条消息可能到达不止一次。
- 思路
- 把工作设计成执行两次与执行一次效果相同。
- 例子
- 扣款前先检查“订单 42 是否已扣款?”——如果是,就跳过。
- 代价
- 你需要在某个地方记住哪些事已经做过。
- 关联
每个 Consumer 都必须具备;与 Dead-Letter Queue 一起,让重试变得安全。
Message Ordering——顺序没有保证
按先后顺序寄出的两封信,到达时可能是任意顺序——所以如果第二步比第一步先到,收信人就会一头雾水。
消息很多、consumer 又并行时,消息不一定按发送的顺序被处理。如果“更新地址”和“删除账号”乱序到达,结果就是一团乱。有些系统能以一定代价在某个类别内保持顺序;通常更省事的办法是把工作设计成不依赖严格顺序。不管怎样,除非你专门做了安排,否则绝不要假定有顺序。
- 问题
- 并行的 consumer 不会按发送的顺序处理消息。
- 思路
- 不要依赖顺序——或在某个类别内显式地安排顺序。
- 例子
- “删除账号”先于“更新地址”被处理——结果一团乱。
- 代价
- 严格的顺序需要花功夫,还会妨碍并行。
- 关联
- Competing Consumers 的代价;Eventual Consistency 的近邻。
Eventual Consistency——结果最终会一致
一则通知在办公室里传开:有那么一会儿,有些人知道、有些人不知道,直到消息传到每个人,大家又一致了。
因为反应是异步发生的,系统是最终一致的(eventually consistent):event 刚发生后,不同部分可能短暂地不一致——订单已存在,但分析系统还没把它统计进去。它很快就会追上,但“到处都瞬间一致”这件事没有了。你要围绕这个间隙来设计,给用户展示合理的中间状态,而不是假定所有部分在同一瞬间更新。
- 问题
- event 刚发生后,系统的各个部分会短暂地看到不同的东西。
- 思路
- 接受这个间隙:每个部分都会追上并达成一致——只是晚一点。
- 例子
- 订单已创建,但报表里还没有;片刻之后就有了。
- 代价
- 你得为用户设计中间状态(“处理中……”)。
- 关联
- Asynchronous Processing 和 Event-driven Architecture 的结果。
Dead-Letter Queue——有毒的消息需要一个去处
一封没人能处理的信——字迹模糊、根本没法办——不能无休止地重试、堵住整条队伍。它会被放到一个特别的托盘里,等人来查看。
有些消息永远无法被成功处理——格式错误,或者对应的数据已经不存在。如果放任不管,这样的 poison message(毒消息)会被无休止地重试,并堵住 queue。标准的解决办法是 dead-letter queue(死信队列):失败几次之后,把消息挪到一个单独的 queue 里等待检查,这样它就不再毒害整个流程。要为会失败的消息做好打算,否则一条坏消息就会让它后面的所有消息都停滞。
- 问题
- poison message 每次都失败,被无休止地重试,并堵住 queue。
- 思路
- 失败几次之后,把消息挪到一个单独的 queue 里等待检查。
- 例子
- 一条关于已删除用户的消息,尝试三次后进入 DLQ。
- 代价
- 得有人盯着 DLQ,否则它会变成丢失工作的坟场。
- 关联
只有配合 Idempotency,重试才安全;让流程持续运转,从而保障 Resilience。
异步消息带来重复(用 idempotency 应对)、没有保证的顺序、最终一致,以及会失败的消息(用 dead-letter queue)。这四样都要提前规划。
queue 是一件强力工具,不是默认选项。真正的功夫在于:解耦确实划算时才去用它,简单更值钱时就保留直接调用。
Queue vs Direct Call——等待本身是问题时,就用 queue
你领一个号,离开去等一项你稍后再来取的慢服务——但如果只是快速问一句是或不是,你就直接在柜台问,因为领号比等一下还要可笑。
当工作缓慢、有高峰或不紧急,当你想让调用方保持快速,或者当好几个服务都应该对同一件事做出反应时,就用 queue。当你需要立刻得到答复才能继续时——查价格、查权限——就保留直接调用,因为在那里,简单和即时的结果比解耦更值钱。选工具,要看等待是否真的是问题。
- 问题
- 调用方需要答案才能继续吗?
- Queue
- 工作缓慢、有高峰、不紧急,或者很多方都需要(fan-out)。
- 直接调用
- 你需要即时答复:查价格、查权限。
- Queue 的代价
- broker、重复、顺序、eventual consistency、更难追踪。
- 关联
- 在 Synchronous Call 与 Message Queue 之间做选择。
别条件反射般地用它
为了给隔壁桌的人递张字条,装上一整套邮件收发和分拣系统——这套机器的成本,已经超过了它要解决的问题。
queue 会带来实实在在的复杂度:要运行一个 broker,要处理重复和顺序,要围绕 eventual consistency 做设计,而且追踪一个现在要在消息之间跳转的请求也更困难。对于一个简单的系统、两部分之间只是一次快速调用,这些开销不值得。当某个具体的好处——韧性、规模、解耦——足以证明代价合理时,再加 queue,而不是因为事件驱动听起来很高级。
这些概念是相互关联的
一条传送带:从一个痛点到一剂解药——而这剂解药有它自己的好处,也有它自己的副作用。
整体画面如下。synchronous call 的痛点,由 message queue 来治,并由 broker 保管。command(给一个 consumer)和 event(给很多个,经由 pub/sub)从 queue 中流过。这换来了好处——韧性、削峰、扩展——也带来了难题:重复、顺序、eventual consistency 和 DLQ。
- 等待是不是问题——这项工作是否足够缓慢、有高峰或不紧急,值得解耦?
- command 还是 event——是一个 worker 做一项任务,还是许多服务对一个事实做出反应?
- 工作是否幂等——既然消息可能到达两次,执行两次是否安全?
- 它是否假定了顺序,我是安排好了顺序,还是在设计上消除了对顺序的依赖?
- 系统能否容忍短暂的最终不一致?
- 失败的消息去哪儿——有没有 dead-letter queue?
- 你把缓慢、有高峰、不紧急的工作放进 queue,把直接调用留给即时答复。
- 你的 consumer 是幂等的,所以重复消息不会造成伤害。
- 除非明确安排过,否则你不假定有顺序。
- 你为 eventual consistency 做设计,而不是期待瞬间一致。
- 失败的消息落进 dead-letter queue,而不是堵住队伍。
速查表:全部 21 个概念
直接调用的问题
- Synchronous Callrequest–response
调用并等待。简单,但在时间上把服务耦合在一起。
- Failure Propagationcascading failure
一个服务出故障,会拖垮所有调用它的服务。
- Background Workbackground jobs
用户不该等的工作:邮件、发票、照片。
queue
- Message Queuequeue
丢下一条消息就继续走;接收方自己来取。
- Temporal Decouplingbuffer in time
一个缓冲:双方不必在同一时刻运行。
- Asynchronous Processingasync
不等结果;结果在之后发生。
角色
- Producer / Consumerpublisher / worker
一边写消息,一边处理消息;它们只认识 queue。
- Message Brokerbroker
可靠地存储和投递消息的软件:RabbitMQ、Kafka。
- Competing Consumersworker pool
一个 queue 上的多个 worker 分担负载。
command 与 event
- Command做这件事
“做这件事”——恰好一个 consumer 去做。
- Event这件事发生了
“这件事发生了”——任何人都可以反应,也可以没人反应。
- Publish/Subscribepub/sub, topic, fan-out
一个 event 进入 topic——每个订阅者都拿到自己的副本。
- Event-driven ArchitectureEDA
功能作为新的监听者加入,而不是去修改发布者。
好处
- Resiliencefault tolerance
consumer 崩溃只是延迟,不是丢失工作。
- Queue-Based Load Levelingload leveling, backpressure
高峰填满 queue;consumer 平稳地把它排空。
- Independent Scalingindependent evolution
每一边各自扩展、各自变化。
难题
- Idempotencyat-least-once
重复会发生——让它们变得无害。
- Message Orderingordering guarantees
除非你做安排,否则顺序没有保证。
- Eventual Consistencyconsistent in the end
系统的各部分短暂地不一致,然后追上。
- Dead-Letter QueueDLQ
一个专门存放反复失败的消息的 queue。
选择
- Queue vs Direct Call如何选择
需要答案才能继续——直接调用;不需要——queue。
queue 是深思熟虑的选择:当在时间上解耦能换来韧性、规模或 fan-out 时再用它——而当即时答复比这一切都重要时,就保留直接调用。