摘要:数据拆开后最扎心的体验是——从前一条 SQL join 就能拿到的"订单详情页",现在要跨五个服务去凑。本文讲三招:服务端聚合、只读副本/物化视图、以及 CQRS 的读写分离,帮你从"跨服务 join"里爬出来,也给"拆得太碎导致查询爆炸"踩一脚刹车。
数据切开了,写没问题(各写各的),读却翻车了。想象订单详情页:要有订单、买家昵称、商品图、物流信息、支付状态——而在微服务里,这些分属五个服务的五张表。你在单体时代一句 left join 就搞定,现在得想办法"把拼图拼回去"。这类问题,叫跨服务聚合查询。
让一个"聚合服务"攒数据:它先调订单服务拿订单主体,再调用户服务拿昵称,最后调商品/物流服务凑齐素材,拼成一个完整详情返回给前端。这是最直觉的做法,也是中小系统里性价比最高的起步。
代价很明显:每个详情页都要串一次多个服务的调用,延迟是叠加的,而且一次五六个同步来回,本身就考验你第 3 章学的超时/重试/熔断功力。所以服务端聚合适合"低频、数据不大"的详情场景;高频热点详情,得靠下两招。
思路是把"高频读"和"低频写"解耦——用一个服务自己长出的只读副本,替掉每次都要跨库 join。做法:当业务事件发生(比如订单创建、物流更新),把相关数据"投影"到当前服务自己的只读表里,前端读这个本地副本,又快又不打扰源头服务。
# 示意:只读副本是"从事件推到本地",不是每次都跨服务 join on(OrderCreated) -> write order_cart_snapshot(本服务只读表) on(LogisticsUpdated)-> update order_cart_snapshot.delivery # 前端查 order_cart_snapshot 即可,无需再跨国五个服务
这份副本必然有一点滞后(它是事件的投影,不是源表),但对详情页、列表页这类可容忍稍旧的数据绰绰有余。用它换掉每一次高成本的多服务加载,是拆服务后最重要的"读取去重"。这也正是"最终一致"的一个甜果子:副本稍旧没关系,账最终对得上。
物化视图只是"一个副本",CQRS 把"读"和"写"彻底分家,是更完整的一套姿势。
CQRS 的全称是命令查询职责分离:写(Command)走一条路,读(Query)走另一条路。它们可以不共享同一个模型——写端用适合写的领域模型,读端用自己的只读投影模型。放到微服务里,常见做法是"业务服务负责写 + 发布事件,查询服务订阅事件并维护自己的读模型"。
好处:读压力被隔离到专门的读服务,写服务不必为高频查询牵肠挂肚;读模型可以按界面来建模(详情页长什么样就存成什么样),查询不再是痛苦的跨服务拼装。代价:系统复杂度上去了——要维护事件同步、最终一致的读模型、处理投影故障。CQRS 不是读到慢就上的银弹,而是当你发现"查询需求花样多、常常要跨服务拼",又承受得起复杂度时再启动的工程。
给它们排个序,省得你连 CQRS 都还没学会就冲上去:
| 方案 | 适用 | 代价 |
|---|---|---|
| 服务端聚合 | 低频详情的兜底 | 延迟叠加,依赖多重调用健壮 |
| 只读副本 | 高频读、可容忍稍旧 | 要写事件投影,有轻微滞后 |
| CQRS 读写分离 | 查询花样多、经常跨服务拼 | 架构复杂度高,维护成本大 |
实战路径建议:先服务端聚合撑住基本盘;热点详情上只读副本;等读取真成了系统负担、产品又经常要新报表时,才考虑完整 CQRS。别为了"架构看起来高级"而跳到 CQRS——那会让你提前背上最重的一副担子。
CQRS 最常见翻车,是只做了"读模型"却没想清楚"读模型怎么被更新"。事件丢了、投影挂了、更新顺序乱了,读模型和源表对不上——用户看到的订单状态是错的。CQRS 的成败很大程度取决于事件可靠性 + 投影的幂等与重建能力(这正好接住第 3 章异步通信里讲的 outbox 和消息可靠性)。读模型不是"抄自动生成的",它是你主动维护的资产,要有对账与自愈手段。
读模型、副本、CQRS 各有各的滞后,选哪个其实被一个隐藏前提卡着:你的产品接受多久之前的数据? 详情页允许"延迟几秒钟"挺好;财务对账、库存扣减读不准当场就可能出事。所以动手前先把每个读取场景按"可容忍的新鲜度"分档:秒级可容忍,就放心上只读副本;必须零延迟强一致(哪怕再慢),那就别勉为其难走异步投影,老老实实回同步聚合查询。这个分档做在前面,能帮你避免"为了快而选了个会给出错数据的方案"这种本末倒置。
如果连一张报表都要走跨服务拼装的路线,说明你很可能已经在为错误的边界买单——这张报表要的数据本质上属于同一个业务视图,却被切进了好几个上下文。此时与其硬着头皮在查询层做高成本拼接,不如反向问一句:这张报表该由谁拥有?它通常是"读模型归某个团队专门维护"的正解:让一个负责该报表的团队把相关事件订阅进来,建成自己的本地读模型,报表查询只打它一个服务。与其把读固化成某个专门服务的私有资产,不如让每个高频读截面都有一块自己的本地影子,这是治查询爆炸最省钱的一条路。
别等出问题再想读取面:拆服务的同时就该想清楚详情页谁来拼
拆服务后读查询从一句 join 变成跨服务拼装,是必然的新难题
服务端聚合是最简单兜底,适合低频详情,代价是延迟叠加
只读副本用"事件投影"把高频读本地化,是读取去重的主力
CQRS 把读写彻底分家,查询靠读模型,复杂且需事件可靠
排序:先聚合→再副本→实在必要才 CQRS,别为高级而冲