拆微服务时,我们纠结了半年的订单-商品-用户边界,最后被一张数据库表说服了

上周三下午,团队决定把拆了半年的订单服务重新合并回去。

所有人都沉默了。不是因为失败,而是因为那张表——我们盯着数据库里的order_item_snapshot表看了十分钟,终于意识到一个被我们刻意忽略的事实:订单里的商品快照,从来就不属于商品服务。

事情要从去年九月说起。彼时我们的单体应用已经膨胀到每次部署需要四十分钟,CI/CD流水线动不动就挂,测试环境三天两头被人搞坏。拆微服务是共识,问题是怎么拆。CTO给了方向:按领域拆,订单、商品、用户,三个核心域先拆开。

会议室白板上画了两套方案。

第一套方案来自架构组,标准DDD打法:订单服务、商品服务、用户服务各自独立,每个服务拥有自己的数据库,服务间通过API通信。订单需要商品信息?调商品服务。订单需要用户地址?调用户服务。干净、解耦、符合教科书。

第二套方案来自产品组的老李,他画了一条用户下单的流程图,从浏览商品到支付完成,画了十七个步骤。他说你们按领域拆,用户从头到尾要跨四个服务,一次下单至少六次远程调用,你们算过延迟吗?

架构组算了一下,按p99延迟算,商品查询80ms,库存扣减120ms,优惠计算150ms,再加序列化开销,光服务间调用就奔着500ms去了。而当时单体里整个下单流程平均耗时340ms。

老李的方案是按用户旅程拆:浏览发现一个服务、下单支付一个服务、售后履约一个服务。每个服务内部聚合自己需要的数据,尽量减少跨服务调用。

两派吵了两个月。

架构组坚持领域纯粹性:"商品信息就应该归商品服务管,订单服务存商品ID就行了,需要详情去查。"老李反问:"那订单列表页呢?一个用户有五十个订单,每个订单有三个商品,你让前端调多少次商品服务?"架构组说可以批量查询。老李说那商品下架了呢?商品改名了呢?用户看到的历史订单里商品名称全变了,这事你负责?

这击中了要害。电商系统的核心矛盾在于:商品信息是动态变化的,但订单是历史事实。用户半年前买了一件"2024春季新款风衣",现在这件商品在商家后台被改成了"2025春季经典款风衣",用户打开历史订单看到名字变了,会觉得系统出了bug。

架构组提出了一个折中方案:订单服务在创建时缓存商品信息。但缓存就意味着数据冗余,意味着一致性需要额外维护,意味着领域边界其实已经模糊了。那为什么还要死守"商品信息归商品服务"这条线?

转折点出现在一次生产事故之后。

双十一预热期间,商品服务因为商家批量改价被打挂了,订单服务的商品查询接口全部超时。用户看不到订单详情,客服电话被打爆。运维紧急扩容商品服务节点,但数据库连接池先撑不住了——商家端的商品编辑和用户端的订单查询抢同一批数据库连接。

事故复盘会上,DBA老张把数据库慢查询日志投影到屏幕上,指着一条SQL说:这是订单详情页的查询,join了orders、order_items、products三张表,跑了2.7秒。你们说的微服务解耦,数据库层面全耦合在一起。

然后他打开了另一张表的DDL。

CREATE TABLE order_item_snapshot (
    id BIGINT PRIMARY KEY,
    order_id BIGINT NOT NULL,
    product_id BIGINT NOT NULL,
    product_name VARCHAR(200) NOT NULL,
    product_main_image VARCHAR(500),
    sku_spec VARCHAR(300),
    unit_price DECIMAL(10,2) NOT NULL,
    snapshot_version INT DEFAULT 1,
    created_at DATETIME NOT NULL,
    INDEX idx_order_id (order_id)
);

这张表存在了两年,是之前一个离职的同事为了做订单商品快照偷偷加的。他的逻辑很简单:下单那一刻,把商品的名称、图片、规格、单价全部拍一张快照存下来,之后订单相关的所有展示都读这张表,永远不关联products表。

全会议室安静了大概三十秒。

老李开口了:"所以问题不是按领域拆还是按旅程拆,而是我们一直在用一个错误的模型理解'商品'这个概念。"

他站起来在白板上写了两个词:Product Catalog 和 Product Snapshot。

Product Catalog属于商品服务,管的是商家怎么描述商品、怎么定价、怎么上下架。这是动态的、可变的。

Product Snapshot属于订单服务,存的是用户下单那一刻商品长什么样。这是静态的、不可变的,是法律意义上的交易凭证。

这两个东西虽然都叫"商品",但它们的生命周期、变更频率、业务含义完全不同。强行归到一个服务里,用一个模型去表达两种截然不同的业务实体,这才是真正的问题。

接下来的三周,我们重新梳理了数据边界。核心原则就一条:数据跟着它的变更原因走

商品名称为什么会变?因为商家要优化标题、更新卖点。变更的驱动方是商家,消费者是变更的被动接受者。所以商品名称的管理归属商品服务。

订单里的商品快照为什么会变?它不应该变。即使商品下架了、改名了、涨价了,订单里记录的仍然是交易发生时的真实信息。它的"不变"本身就是业务需求。所以快照归属订单服务。

类似的分析被应用到其他字段上:

  • 商品价格:动态价格(商家可以随时改)归商品服务;成交价格(下单时锁定的价格)归订单服务,存在快照里。
  • 用户地址:用户的地址簿归用户服务,用户可以随时增删改;订单的收货地址归订单服务,下单时从用户服务复制一份快照存下来。
  • 用户昵称:用户服务管,但订单里的买家昵称下单时快照一份。用户后来改昵称不影响历史订单展示。

这个原则听起来简单,但在实际落地时我们犯过一个错。订单快照里最初只存了product_id和快照字段,没有存sku_spec。结果当商家修改了SKU规格名称(比如把"大杯"改成"500ml"),历史订单里的规格显示也跟着变了。因为前端取到product_id后又去商品服务查了最新的SKU信息。

修复很简单:快照必须覆盖订单详情页需要展示的每一个字段,不允许任何回源查询。这是硬规则。

最终的服务划分长这样:

  • 商品服务:管理商品目录,包括SPU、SKU、类目、属性、价格、库存。商家端的增删改查全在这里。
  • 订单服务:管理交易生命周期,拥有自己的数据库,核心表包括orders、order_items、order_item_snapshot、order_delivery_snapshot。下单时从商品服务和用户服务获取需要快照的数据,写入本地表后,后续所有读操作不依赖外部服务。
  • 用户服务:管理用户账户、地址簿、会员等级。与订单服务的关系是:下单时提供数据,订单创建后各自独立演化。

商品服务和订单服务之间的API只有两个:下单前校验(查库存、验价格)和下单时获取快照数据。这两个接口都是同步调用,因为下单链路本身需要强一致性。其他场景——比如订单列表、订单详情、售后申请——全部读订单服务自己的快照表,零外部依赖。

上线后订单详情页的p99延迟从之前的680ms降到了110ms。不是因为微服务架构有多好,而是因为不需要跨服务join了。

至于之前担心的数据冗余导致的一致性问题,实际上根本不存在。因为快照的本质就是"不允许一致"。商品改了就改了,订单里存的本来就是历史状态,两者不需要同步。我们花了半年纠结怎么保持数据一致,最后发现这个场景下"不一致"才是正确行为。

回过头看,这场争论的根源是我们被"领域"这个词困住了。DDD的聚合根、限界上下文确实是好工具,但前提是你对业务实体的生命周期有足够深刻的理解。同一个"商品",在商家视角和买家视角下是完全不同的概念,硬塞进一个服务里,无论怎么设计都会别扭。

那张order_item_snapshot表现在还跑在生产环境里,字段从最初的七个扩展到了十五个,增加了税费信息、优惠分摊、发货仓库这些下单场景必需的快照数据。每次有人提议加字段,我们就问一个问题:这个信息在下单那一刻确定之后还会不会变?如果不会变,就进快照表。

上个月架构评审,一个新同事问:快照表里的商品图片URL,如果CDN域名迁移了怎么办?老李说那是CDN层的事,跟业务数据没关系。但如果图片本身被替换了呢?那快照里存的就是旧URL,用户看到的就是下单时的商品图——这正是我们要的效果。


常见问题

你们怎么处理商品下架后的订单快照展示?

快照表不依赖商品服务的任何数据,商品下架、删除都不影响。订单详情页、列表页、售后页面全部从order_item_snapshot和order_delivery_snapshot取数据,主图、标题、规格、价格都是下单那一刻的值。唯一的例外是商品链接——如果商品已删除,前端会展示为不可点击的纯文本。

下单快照的数据量会不会很大?

按日均十万单、每单平均三个商品计算,order_item_snapshot表每天新增三十万行,单行数据约1KB,三年累计约3亿行、300GB。我们用order_id做分区键,按月分区,归档策略是保留热数据六个月,超过六个月的迁移到冷存储。订单列表的分页查询走ES索引,不直接扫快照表。

快照和商品服务的数据如果出现法律合规问题怎么办?比如商品描述快照时是"纯棉",后来商品服务里改成了"涤纶",消费者投诉虚假宣传以哪个为准?

以快照为准。快照是交易凭证,记录的是交易发生时的商品信息。这也是快照独立于商品服务的法理依据——不能因为商家事后修改商品描述就改变了历史交易的客观事实。我们甚至在下单接口里加了快照版本号,每次商品关键信息变更时自增版本号,确保快照能追溯到商品变更历史。

如果下单时需要快照的字段越来越多,订单服务的边界会不会膨胀?

会,但这是合理的膨胀。快照字段的增加对应的是"订单详情页需要展示的信息变多了",这本身就是订单服务的职责范围。真正的边界问题出现在订单服务开始管理商品类目树或用户积分规则的时候——那才是越界。我们判断的标准很简单:这个数据的变更驱动方是谁?如果是买家下单行为触发的记录,归订单服务;如果是商家运营行为触发的修改,归商品或用户服务。