<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>微服务 on 方糖猫Blog</title>
    <link>https://ftmao.com/blog/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/</link>
    <description>AI 视频、AI Agent 与工程实践笔记</description>
    <generator>Hugo</generator>
    <language>zh-CN</language>
    <lastBuildDate>Mon, 19 Oct 2026 10:00:00 +0800</lastBuildDate><atom:link href="https://ftmao.com/blog/tags/%E5%BE%AE%E6%9C%8D%E5%8A%A1/index.xml" rel="self" type="application/rss+xml" /><item>
      <title>C 端 IPTV 产品的 Java 高并发架构设计：高可用、高性能、零信任与缓存一致性</title>
      <link>https://ftmao.com/blog/posts/iptv-java-high-concurrency-architecture/</link>
      <pubDate>Mon, 19 Oct 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/iptv-java-high-concurrency-architecture/</guid>
      <description>&lt;h1 id=&#34;c-端-iptv-产品的-java-高并发架构设计高可用高性能零信任与缓存一致性&#34;&gt;C 端 IPTV 产品的 Java 高并发架构设计：高可用、高性能、零信任与缓存一致性&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;过去几年我一直在做一个面向海外的 C 端 IPTV 产品（STB 机顶盒 + 移动端，多品牌多租户，跑在 AWS 上）。这篇文章是我对这套系统架构的复盘：高并发、高可用、高性能、高安全、缓存一致性这几件事我们分别是怎么做的、踩过什么坑。过程中我们落地了不少 Netflix 的开源组件和公开实践（Hystrix、EVCache、Zuul 的网关思想、混沌工程等），但每一个决策都是经过我们自己生产环境验证或修正的，不是照搬。文中方案细节来自真实的系统设计文档（Video Cloud 目标模型、vod-auth、CloudRouter、DRM、网盘整改等），脱敏后分享。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一先搞清楚对手c-端-iptv-的流量模型&#34;&gt;一、先搞清楚对手：C 端 IPTV 的流量模型&lt;/h2&gt;
&lt;p&gt;设计之前先看清流量的形状。这是我做这套系统第一天就逼自己回答的问题：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;特征&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
					&lt;th&gt;架构含义&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;潮汐效应极强&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;晚 8 点 - 11 点是高峰，大促、球赛、爆款剧上线时流量成倍脉冲&lt;/td&gt;
					&lt;td&gt;弹性伸缩 + 削峰，按高峰容量做预案&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;读极多、写极少&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;EPG 节目单、Meta 元数据、Layout 下发占了 9 成以上流量&lt;/td&gt;
					&lt;td&gt;缓存优先，读路径尽量不打 DB&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;播放鉴权是超级热点&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;每次起播、切台都要鉴权取播放链接，QPS 随在线用户数线性涨&lt;/td&gt;
					&lt;td&gt;热点接口单独优化（缓存 + 短 TTL + 滑动续期）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;设备在线心跳&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;app 轮询 session，百万级设备定时上报&lt;/td&gt;
					&lt;td&gt;心跳服务独立部署，写路径异步化&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;多品牌多租户&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;同一套系统服务多个品牌（巴西、俄语区等）&lt;/td&gt;
					&lt;td&gt;分库隔离爆炸半径，account_id 前缀区分品牌&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这个流量形状——读多写少、晚间高峰、播放路径是生命线——决定了后面所有的架构取舍。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二总体架构一张图看清全貌&#34;&gt;二、总体架构：一张图看清全貌&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    subgraph 终端
        STB[&amp;#34;STB 机顶盒&amp;#34;]
        APP[&amp;#34;移动 App&amp;#34;]
        WEB[&amp;#34;Web / Portal&amp;#34;]
    end

    subgraph 接入层
        ELB[&amp;#34;AWS ELB&amp;#34;]
        APISIX[&amp;#34;APISIX 网关&amp;lt;br/&amp;gt;限流 Sentinel / WAF / 鉴权前置&amp;#34;]
    end

    subgraph 微服务层
        AUTH[&amp;#34;vod-auth&amp;lt;br/&amp;gt;登录注册 / 账号 / 子账号&amp;#34;]
        SESS[&amp;#34;common-session&amp;lt;br/&amp;gt;会话管理&amp;#34;]
        META[&amp;#34;vod-meta-api&amp;lt;br/&amp;gt;元数据 / Layout / EPG&amp;#34;]
        PROF[&amp;#34;profile&amp;lt;br/&amp;gt;观看历史 / 收藏 / 偏好&amp;#34;]
        CLOUD[&amp;#34;vod-cloud&amp;lt;br/&amp;gt;个人网盘&amp;#34;]
        ROUTER[&amp;#34;CloudRouter&amp;lt;br/&amp;gt;播放链接路由&amp;#34;]
        DRM[&amp;#34;drmlicense&amp;lt;br/&amp;gt;DRM 鉴权与 license 下发&amp;#34;]
    end

    subgraph 数据层
        MYSQL[&amp;#34;MySQL RDS&amp;lt;br/&amp;gt;按品牌分库&amp;#34;]
        REDIS[&amp;#34;Redis Cluster&amp;lt;br/&amp;gt;Session / 验证码 / Meta 缓存&amp;#34;]
        METAREDIS[&amp;#34;Meta 专用 Redis Cluster&amp;#34;]
        ES[&amp;#34;Elasticsearch&amp;lt;br/&amp;gt;检索字段&amp;#34;]
        KAFKA[&amp;#34;Kafka MSK&amp;lt;br/&amp;gt;Meta 同步 / 网盘同步&amp;#34;]
    end

    subgraph 内容侧
        STREAM[&amp;#34;Streaming 流媒体&amp;#34;]
        CDN[&amp;#34;CDN / Squid 边缘缓存&amp;#34;]
        CP[&amp;#34;cloudProxy&amp;lt;br/&amp;gt;第三方网盘适配&amp;#34;]
    end

    STB --&amp;gt; ELB
    APP --&amp;gt; ELB
    WEB --&amp;gt; ELB
    ELB --&amp;gt; APISIX
    APISIX --&amp;gt; AUTH
    APISIX --&amp;gt; SESS
    APISIX --&amp;gt; META
    APISIX --&amp;gt; PROF
    APISIX --&amp;gt; CLOUD
    APISIX --&amp;gt; ROUTER
    ROUTER --&amp;gt; DRM
    ROUTER --&amp;gt; STREAM
    STREAM --&amp;gt; CDN
    STREAM --&amp;gt; CP
    META --&amp;gt; METAREDIS
    META --&amp;gt; ES
    AUTH --&amp;gt; MYSQL
    AUTH --&amp;gt; REDIS
    SESS --&amp;gt; REDIS
    CLOUD --&amp;gt; KAFKA
    META --&amp;gt; KAFKA&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;几个关键决策，后面逐章展开：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;接入层&lt;/strong&gt;：链路是 &lt;code&gt;app -&amp;gt; ELB -&amp;gt; APISIX -&amp;gt; vod 微服务&lt;/code&gt;，限流用 Sentinel，网关做鉴权前置和 WAF。网关选型时我认真研究过 Netflix Zuul 2 从同步阻塞改异步非阻塞的历程——结论是网关本身绝不能成为瓶颈。我们体量没那么大，APISIX（底层同样是 Nginx + Lua 的高性能异步模型）已经够用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务拆分&lt;/strong&gt;：按业务域拆（Auth / Profile / Meta / Cloud / Session），不是按技术层拆。领域拆分的完整过程在&lt;a href=&#34;https://ftmao.com/blog/blog/posts/iptv/&#34;&gt;视频系统 DDD 实战&lt;/a&gt;里写过。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据层&lt;/strong&gt;：MySQL 按品牌分库（&lt;code&gt;{brand}_account_db&lt;/code&gt; 等）；Redis 承载 Session、验证码、热点数据；Meta 用&lt;strong&gt;独立的 Redis Cluster&lt;/strong&gt; 全量缓存，与业务缓存物理隔离；检索字段入 ES。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步化&lt;/strong&gt;：Kafka（MSK）只干两件事——Meta 同步管道和网盘同步，不滥用。这是我给自己立的规矩：每加一个 MQ topic 都要回答「同步调用为什么不行」。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三高并发把流量挡在离用户最近的地方&#34;&gt;三、高并发：把流量挡在离用户最近的地方&lt;/h2&gt;
&lt;p&gt;高并发的核心心法就一句：&lt;strong&gt;每一层都尽量把请求消化掉，别让它穿透到下一层&lt;/strong&gt;。越靠近 DB 越贵。&lt;/p&gt;
&lt;h3 id=&#34;31-网关层第一道削峰闸&#34;&gt;3.1 网关层：第一道削峰闸&lt;/h3&gt;
&lt;p&gt;我们在网关层做了三件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;限流&lt;/strong&gt;：APISIX 自带 limit-req / limit-count 插件做粗粒度限流，Sentinel 做服务级细粒度熔断限流（现网组合就是「ELK + Skywalking + Sentinel（APISIX）」）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;鉴权前置&lt;/strong&gt;：明显非法的请求（无 token、签名错误、UA 黑名单）在网关直接 401/403，不放行到业务服务。被攻击时这一层挡掉的流量经常比业务层处理的还多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;静态配置下发走 CDN&lt;/strong&gt;：&lt;code&gt;/ui/cfg/v1&lt;/code&gt;、&lt;code&gt;/layout/v1&lt;/code&gt; 这类终端配置接口，内容变化频率低，直接走 CDN 缓存，源站只承担回源请求。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;32-热点接口播放鉴权链接的缓存设计&#34;&gt;3.2 热点接口：播放鉴权链接的缓存设计&lt;/h3&gt;
&lt;p&gt;CloudRouter（播放源路由服务）是我手里最热的路径：每次起播都要按 &lt;code&gt;logicStreamId&lt;/code&gt; 换真实的下载/播放链接。我做的设计非常直接：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[&amp;#34;App 起播请求&amp;lt;br/&amp;gt;logicStreamId&amp;#34;] --&amp;gt; B{&amp;#34;查 Redis&amp;#34;}
    B -- &amp;#34;命中&amp;#34; --&amp;gt; C[&amp;#34;刷新 TTL 滑动续期&amp;lt;br/&amp;gt;直接返回链接&amp;#34;]
    B -- &amp;#34;未命中&amp;#34; --&amp;gt; D[&amp;#34;查 MySQL 两表&amp;lt;br/&amp;gt;logic_stream + file_source&amp;#34;]
    D --&amp;gt; E[&amp;#34;构建下载链接 DownloadBO&amp;lt;br/&amp;gt;link / fileSize / expired&amp;#34;]
    E --&amp;gt; F[&amp;#34;写回 Redis&amp;#34;]
    F --&amp;gt; G[&amp;#34;返回&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三个细节值得说：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;命中即续期（滑动 TTL）&lt;/strong&gt;：热播内容的链接永不过期，冷内容自然淘汰——不需要人工预热。这个设计替我省掉了整个「热点内容运营」的麻烦。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缓存对象自带过期时间&lt;/strong&gt;：链接本身有 &lt;code&gt;expired&lt;/code&gt; 字段，客户端在链接到期前会重新请求，和缓存 TTL 是两层独立的过期机制，互不相信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;失败快速抛出&lt;/strong&gt;：查不到直接抛业务异常，不做 fallback 到第三方存储的慢路径——播放链路每一毫秒都值钱。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;33-多级缓存l1--l2&#34;&gt;3.3 多级缓存：L1 + L2&lt;/h3&gt;
&lt;p&gt;纯 Redis 方案在超热点 key 上依然会打满单个分片（新片上线当晚的 metaId 我真实遇到过）。为了解决这个问题我研究了 Netflix 的 EVCache——它的多副本加客户端本地缓存思路给了我启发，但我们体量用不上 EVCache 全套，于是我基于 Caffeine（L1 进程内缓存）+ Redis（L2）自己实现了一个两级缓存框架并开源，见&lt;a href=&#34;https://ftmao.com/blog/blog/posts/kecache-intro/&#34;&gt;我开发的 keCache：面向 Spring Boot 3 的 L1 + L2 两级缓存框架&lt;/a&gt;，要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;L1 短 TTL（秒级），扛住瞬时洪峰；&lt;/li&gt;
&lt;li&gt;L1 失效消息走 Redis Pub/Sub 广播，保证多实例间 L1 不一致的窗口可控；&lt;/li&gt;
&lt;li&gt;序列化用 Kryo，比 JDK 序列化小 3-5 倍。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;34-写路径异步化mq-削峰&#34;&gt;3.4 写路径异步化：MQ 削峰&lt;/h3&gt;
&lt;p&gt;写少但集中的场景（新片上线批量导入 Meta、网盘大规模刷新）我全部走 Kafka：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[&amp;#34;vod-sync-dispatcher&amp;lt;br/&amp;gt;每 30 秒轮询上游片源&amp;#34;] --&amp;gt; B[&amp;#34;Kafka MSK&amp;#34;]
    B --&amp;gt; C[&amp;#34;vod-imdb-meta-sync&amp;lt;br/&amp;gt;元数据入 Redis + ES&amp;#34;]
    D[&amp;#34;vod-cloud-sync-dispatcher&amp;#34;] --&amp;gt; B
    B --&amp;gt; E[&amp;#34;vod-cloud-sync-worker&amp;lt;br/&amp;gt;刷新网盘&amp;#34;]
    F[&amp;#34;http2Msk&amp;lt;br/&amp;gt;大数据 HTTP 回调转 MQ&amp;#34;] --&amp;gt; B
    B --&amp;gt; G[&amp;#34;偏好计算&amp;lt;br/&amp;gt;you may like&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;生产者-消费者模型 + MQ 缓冲，让上游脉冲（片源方一次推几万条 meta）被摊平成消费者能吃得动的匀速流。当时的 MQ 选型过程见&lt;a href=&#34;https://ftmao.com/blog/blog/posts/mq-comparison-rabbitmq-kafka-rocketmq/&#34;&gt;消息队列总结：RabbitMQ、Kafka、RocketMQ 与 Redis MQ&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;另一个异步化的例子是我设计的转码调度：&lt;strong&gt;Quartz + DB 状态机&lt;/strong&gt;驱动，每个片源转出 1-3 个分辨率、每个分辨率一个任务，用「动态归一化权重调度算法」分配执行器（priority = 客户权重 / 活跃任务数，兼顾多客户公平与并发槽位控制），30 分钟轮询任务进度。慢不要紧，稳和公平才要紧——这是典型的「离线任务用状态机换可靠性」。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四高可用假设一切依赖都会挂&#34;&gt;四、高可用：假设一切依赖都会挂&lt;/h2&gt;
&lt;p&gt;我做高可用的两条心法，是从 Netflix 的工程实践里学来并在我们系统里反复验证过的：&lt;strong&gt;Design for failure&lt;/strong&gt;（为失败而设计）和 &lt;strong&gt;Blast radius&lt;/strong&gt;（控制爆炸半径）。落地到我们系统：&lt;/p&gt;
&lt;h3 id=&#34;41-熔断降级与兜底重试&#34;&gt;4.1 熔断、降级与兜底重试&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;熔断&lt;/strong&gt;：我们早期直接用 Netflix 开源的 Hystrix 做熔断隔离，Hystrix 停更后迁移到 Resilience4j（网关层用 Sentinel）。工具换了，规则不变：下游超时率/错误率超阈值就熔断，快速失败，绝不线程排队等死。Hystrix 的线程池隔离思想对我们影响很大——核心链路（登录、起播）和非核心链路（埋点上报）的资源池必须分开。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;降级而非失败&lt;/strong&gt;：Meta 自动匹配失败时我定的策略是&lt;strong&gt;降级创建默认元数据&lt;/strong&gt;，不影响文件源状态——内容能播永远比信息完美重要。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;兜底重试&lt;/strong&gt;：CloudAsset 的异常处理是我给全系统定的统一模式——&lt;strong&gt;3 次以内本轮调度重试，超过 3 次任务进失败状态，由独立的失败任务调度器兜底&lt;/strong&gt;。关键设计点：主流程不被重试拖死，失败任务有独立通道排队消化，互不阻塞。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    A[&amp;#34;任务执行&amp;#34;] --&amp;gt; B{&amp;#34;成功?&amp;#34;}
    B -- &amp;#34;是&amp;#34; --&amp;gt; C[&amp;#34;完成&amp;#34;]
    B -- &amp;#34;否, failCount+1&amp;#34;] --&amp;gt; D{&amp;#34;failCount ＞ 3?&amp;#34;}
    D -- &amp;#34;否&amp;#34; --&amp;gt; A
    D -- &amp;#34;是&amp;#34; --&amp;gt; E[&amp;#34;status = fail&amp;#34;]
    E --&amp;gt; F[&amp;#34;独立失败任务调度器&amp;lt;br/&amp;gt;定时扫描兜底重试&amp;#34;]
    F --&amp;gt; A&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;后来我把这类「一定要跑到」的可靠执行逻辑抽成了通用组件开源，见&lt;a href=&#34;https://ftmao.com/blog/blog/posts/ke-reliable-executor/&#34;&gt;我开发的 KE Reliable Executor&lt;/a&gt;。&lt;/p&gt;
&lt;h3 id=&#34;42-多租户隔离分库就是隔舱&#34;&gt;4.2 多租户隔离：分库就是隔舱&lt;/h3&gt;
&lt;p&gt;多品牌共享一套代码，但我在数据层做了物理隔离：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MySQL 按品牌分库（&lt;code&gt;{brand}_account_db&lt;/code&gt;、&lt;code&gt;{brand}_profile_db&lt;/code&gt;），公共数据在 &lt;code&gt;vod_meta_db&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;account_id&lt;/code&gt; 前缀区分品牌（MFC:11、TVELA:12、WAKA:13），应用层强制路由；&lt;/li&gt;
&lt;li&gt;实体普遍带 &lt;code&gt;env&lt;/code&gt; / &lt;code&gt;vnoId&lt;/code&gt; 字段，&lt;code&gt;logicStreamId&lt;/code&gt; 内嵌环境标识。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;某品牌数据被打爆、误删、需要回滚时，影响面天然被限制在单库内。这就是水密隔舱——后来某品牌做数据订正时我真切体会到这个设计的价值。&lt;/p&gt;
&lt;h3 id=&#34;43-依赖解耦订阅模式&#34;&gt;4.3 依赖解耦：订阅模式&lt;/h3&gt;
&lt;p&gt;DRM 链路里我有个坚持：drmlicense 不直接依赖 Headend 和 drmencryptor 的在线接口，而是以&lt;strong&gt;订阅模式&lt;/strong&gt;收 &lt;code&gt;DrmContentInfoBO&lt;/code&gt; 和 &lt;code&gt;ContentKeyBO&lt;/code&gt;，自己组装 logicalStream 与 PSSH 的关系。上游挂了，license 下发用已订阅到的数据照常工作。原因是算术：&lt;strong&gt;同步调用是可用性的乘法，异步订阅是加法&lt;/strong&gt;——一个可用性 99.9% 的服务同步串依赖十个同样 99.9% 的服务，链路可用性就只剩 99% 了。&lt;/p&gt;
&lt;h3 id=&#34;44-混沌工程轻量版就够&#34;&gt;4.4 混沌工程：轻量版就够&lt;/h3&gt;
&lt;p&gt;Netflix 的 Chaos Monkey 是随机杀生产实例，逼所有服务习惯「邻居随时消失」。我评估后没敢直接上生产，但保留了这个思想的两个轻量实践：预发环境定期随机关停单个实例观察告警与自愈；每个季度做一次依赖演练（手动切断 Redis 从库、Kafka 单 broker），验证降级路径真的能用。我的原则：&lt;strong&gt;降级方案不演练就等于没有&lt;/strong&gt;——每次演练几乎都能发现某条降级路径已经悄悄失效。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五高性能把钱花在读路径上&#34;&gt;五、高性能：把钱花在读路径上&lt;/h2&gt;
&lt;p&gt;高性能章节不长，因为大部分事前面已经做了。补几个我做过的针对性的点：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;优化点&lt;/th&gt;
					&lt;th&gt;做法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Meta 全量入内存&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;元数据全量进专用 Redis Cluster，终端 &lt;code&gt;/root/v1&lt;/code&gt;、&lt;code&gt;/tree/v1&lt;/code&gt; 等接口零 DB 访问&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;检索走 ES&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;只有关键检索字段入 Elasticsearch，不全量双写&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;播放内容边缘缓存&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Streaming 前置 Squid + CDN，热点片源命中边缘节点，源站只承担回源（对应 Netflix Open Connect 的角色，我们用商用 CDN 实现）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;大表治理&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;观看历史表现网 2-3 亿行，是我一直盯着的最大风险点：每天定时任务删 90 天前历史 + 按账号双表拆分（设备账号/普通账号）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;JVM 与连接池&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;G1/ZGC、HikariCP 连接池按「连接数 = (核数 × 2) + 磁盘数」起步压测调优，慢 SQL 进 Skywalking 告警&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;原则：&lt;strong&gt;性能优化的 80% 是让请求根本到不了慢的地方&lt;/strong&gt;，剩下 20% 才是调参数。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六缓存一致性与缓存高可用&#34;&gt;六、缓存一致性与缓存高可用&lt;/h2&gt;
&lt;p&gt;这是全文我最想细说的一章。缓存是性能的来源，也是我事故最多的地方。&lt;/p&gt;
&lt;h3 id=&#34;61-一致性策略先写-db后删缓存&#34;&gt;6.1 一致性策略：先写 DB，后删缓存&lt;/h3&gt;
&lt;p&gt;vod-auth 的账号数据我用的是标准的 &lt;strong&gt;Cache Aside + 写后删除&lt;/strong&gt;：先写 DB，成功后再删缓存，下次读时回源重建。为什么选「删除」而不是「更新」？我想清楚过：「先写 DB 再删缓存」只有在「读线程恰好踩着写间隙回源了旧值」的极端窗口才会短暂不一致，而「更新缓存」在并发双写下会乱序覆盖出脏数据。删除的失败模式是 miss（回源一次，便宜），更新的失败模式是脏数据（播错内容，贵）。&lt;/p&gt;
&lt;p&gt;对一致性要求更高的场景（套餐权益、付费状态），我在删除之上再加一层：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;延迟双删&lt;/strong&gt;：写 DB → 删缓存 → 延迟 N 秒 → 再删一次，兜住间隙回源；&lt;/li&gt;
&lt;li&gt;或用 MQ 发缓存失效消息，由消费者负责删除 + 失败重试，把「删缓存失败」这个尾巴也闭环掉。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;62-缓存三大经典问题的组合拳&#34;&gt;6.2 缓存三大经典问题的组合拳&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;问题&lt;/th&gt;
					&lt;th&gt;我遇到的场景&lt;/th&gt;
					&lt;th&gt;解法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;穿透&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;恶意刷不存在的 metaId / accountId&lt;/td&gt;
					&lt;td&gt;布隆过滤器拦截 + 空值短 TTL 缓存（30 秒）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;击穿&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;爆款剧上线，热点 key 过期瞬间&lt;/td&gt;
					&lt;td&gt;逻辑过期 + 互斥重建：value 里带逻辑过期时间，过期后只放一个线程回源，其余返回旧值&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;雪崩&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;大量 key 同 TTL 同时到期&lt;/td&gt;
					&lt;td&gt;TTL 加随机抖动（基础值 ± 20%）+ Redis Cluster 多主分摊&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;逻辑过期方案有个工程细节：回源重建要用分布式锁（Meta 绑定我用的就是分布式锁，获取失败快速失败、重新调度），且锁的 TTL 必须大于回源耗时，finally 里释放——我在 CloudAsset 方案里专门写了「任何异常都会释放分布式锁、清除 ThreadLocal」，这是血泪经验，锁泄漏一次就够排查一晚上。&lt;/p&gt;
&lt;h3 id=&#34;63-缓存自身的高可用&#34;&gt;6.3 缓存自身的高可用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Redis Cluster 多主 + 从&lt;/strong&gt;：Meta 缓存我用独立集群，和 Session/验证码的集群物理隔离——Meta 集群被打挂不影响用户登录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多级缓存兜底&lt;/strong&gt;：Redis 整体不可用时，L1（Caffeine）还能扛住读过期窗口，服务降级而不是全灭。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;从 EVCache 学到的运营观&lt;/strong&gt;：我研究 Netflix EVCache 时最大的收获不是 memcached 本身，而是&lt;strong&gt;他们把缓存当成 tier-0 服务运营&lt;/strong&gt;——独立容量规划、独立监控、独立 SLO。我照这个思路给缓存集群配了和业务服务同级别的告警与容量评审。缓存在高并发系统里不是优化项，是一级基础设施——这点很多团队要到出事才懂，我庆幸自己学得早。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七零信任安全与必要防御&#34;&gt;七、零信任安全与必要防御&lt;/h2&gt;
&lt;h3 id=&#34;71-零信任永不信任始终验证&#34;&gt;7.1 零信任：永不信任，始终验证&lt;/h3&gt;
&lt;p&gt;我们的安全体系是在几次整改后逐步按零信任思路重建的——内网不再等于可信，&lt;strong&gt;每一次调用都要验证身份，不论它来自哪里&lt;/strong&gt;。落地到我的系统，是这几件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;用户侧&lt;/strong&gt;：accessToken（JWT，&lt;code&gt;sub = accountId&lt;/code&gt;）+ 播放票据 tk12（内含 &lt;code&gt;expireTime / accountId / streamId&lt;/code&gt;，由平台密钥签发）。App 起播时两个都要验，token 验「你是谁」，tk12 验「你能不能播这条流、能播到什么时候」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;设备侧&lt;/strong&gt;：账号卡与设备绑定（&lt;code&gt;ac_device_binding&lt;/code&gt;），&lt;code&gt;device_limit&lt;/code&gt; 限制并发设备数，挤掉盗号共享的盈利空间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务间&lt;/strong&gt;：内部接口（&lt;code&gt;/internal-api/...&lt;/code&gt;）不走公网网关，服务间调用带服务身份凭证。我评估过 mTLS + SPIFFE 这套工作负载身份方案（Netflix 零信任改造用的就是这条路），对我们体量偏重，最终落地为「内部接口不暴露 + 调用方白名单」，等规模上来再升级。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最小权限&lt;/strong&gt;：cloudProxy 是我专门做的收敛层——把对第三方网盘（Mega 等）的所有访问收敛成一个适配层，只暴露 &lt;code&gt;checkPassword&lt;/code&gt; 和 &lt;code&gt;getDownloadLink&lt;/code&gt; 两个接口，业务服务根本不接触第三方凭证。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;72-一次真实的安全整改网盘密码泄露&#34;&gt;7.2 一次真实的安全整改：网盘密码泄露&lt;/h3&gt;
&lt;p&gt;这是我亲手推动的整改，值得完整复盘。背景：网盘打包工具生成的加密网盘包里&lt;strong&gt;含账号 + 加密后的登录密码&lt;/strong&gt;，加解密密钥一旦泄露，所有网盘账号密码全部泄露。&lt;/p&gt;
&lt;p&gt;整改分两步走：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第一步，密码不落地。&lt;/strong&gt; 新网盘包不再写密码，改为写入与网盘账号一一对应的 &lt;code&gt;shareKey&lt;/code&gt;（UUID）。导入时只校验 shareKey 一致性，通过后把网盘与用户关联。按网盘包 &lt;code&gt;version&lt;/code&gt; 字段区分新老包走新旧两条校验逻辑，平滑迁移——&lt;strong&gt;存量兼容是安全改造里最容易被低估的工作量&lt;/strong&gt;，这一步我花的精力比新逻辑本身还多。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第二步，密钥体系升级。&lt;/strong&gt; 对称加密改 RSA 非对称加密：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    A[&amp;#34;CP / Reseller&amp;lt;br/&amp;gt;只拿到 RSA 公钥&amp;#34;] --&amp;gt; B[&amp;#34;公钥加密网盘包&amp;lt;br/&amp;gt;公钥泄露也无法解包&amp;#34;]
    C[&amp;#34;VNO 上传私钥&amp;lt;br/&amp;gt;加密保存于平台 pkg_rsa_key 表&amp;#34;] --&amp;gt; D[&amp;#34;导入时按 VNO 查私钥&amp;#34;]
    B --&amp;gt; E[&amp;#34;网盘包带签名&amp;lt;br/&amp;gt;含 pkgId + keyId&amp;#34;]
    E --&amp;gt; D
    D --&amp;gt; F[&amp;#34;先验签再解密&amp;lt;br/&amp;gt;解密成功才关联用户&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;配套我设计的&lt;strong&gt;密钥轮换机制&lt;/strong&gt;：平台同时最多两个 Key 生效；更新时旧密钥先进入最长 30 天缓冲期，再生成新密钥对；存在生效中的旧密钥则不允许再次更新——防止轮换频率失控导致历史包无法解密。&lt;/p&gt;
&lt;p&gt;另外扫码登录链路我也推动做过同类加固：扫码站点从 S3 静态托管（明文登录）迁移到托管服务并接入加固方案，鉴权统一收口到 vod-auth。&lt;/p&gt;
&lt;h3 id=&#34;73-接口消息体加密https-之上的第二道锁&#34;&gt;7.3 接口消息体加密：HTTPS 之上的第二道锁&lt;/h3&gt;
&lt;p&gt;有人会问：全站都上 HTTPS 了，为什么还要在应用层再加密一次消息体？我的理由有三个，都来自实际教训：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;HTTPS 防不住终端侧的抓包&lt;/strong&gt;。Charles/Fiddler 挂个代理证书，App 和网关之间的明文报文一览无余——协议字段、业务参数、token 用法全暴露。C 端 App 发出去的那一刻起，客户端环境就不可信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTPS 到网关就终止了&lt;/strong&gt;。TLS 在 ELB/APISIX 卸载后，内网转发的是明文；零信任的原则是内网也不可信，敏感报文在网关之后依然应该处于加密状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPTV 场景的特殊性&lt;/strong&gt;：播放鉴权、充值、账号接口一旦被逆向出报文格式，黑产就可以脱离 App 直接伪造请求刷接口。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我设计的方案是 &lt;strong&gt;TLS 为底线 + 应用层 AES 加密消息体&lt;/strong&gt;，核心链路如下：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[&amp;#34;App 启动&amp;lt;br/&amp;gt;ECDH 密钥协商&amp;lt;br/&amp;gt;网关公钥经 SSL Pinning 校验&amp;#34;] --&amp;gt; B[&amp;#34;得到会话密钥 sessionKey&amp;lt;br/&amp;gt;按 session 绑定&amp;#34;]
    B --&amp;gt; C[&amp;#34;业务请求&amp;lt;br/&amp;gt;AES-256-GCM 加密 body&amp;#34;]
    C --&amp;gt; D[&amp;#34;明文头带 token + 时间戳&amp;lt;br/&amp;gt;+ nonce + HMAC 签名&amp;#34;]
    D --&amp;gt; E[&amp;#34;网关统一解密过滤器&amp;lt;br/&amp;gt;验签名 / 验时间窗 / 验 nonce&amp;#34;]
    E --&amp;gt; F[&amp;#34;业务服务收到明文&amp;lt;br/&amp;gt;响应同样加密回传&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;工程上的几个取舍，都是试出来的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;密钥协商用 ECDH，不用硬编码密钥&lt;/strong&gt;。早期图省事在 App 里内置过 AES 密钥，App 被反编译后全线失守，改成登录/启动时 ECDH 动态协商、密钥绑定 session、服务端定期强制轮换。SSL Pinning（证书固定）挡住大部分中间人代理，让 ECDH 协商本身不被劫持。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AES-256-GCM 而不是 CBC&lt;/strong&gt;：GCM 自带完整性校验（AEAD），加密和防篡改一次完成；密钥 nonce 绝不复用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;头部明文、body 加密&lt;/strong&gt;：token、时间戳、nonce、签名放明文头里，网关不解密就能先做限流和鉴权前置；body 整体加密，解密收敛在网关的统一过滤器里，业务服务无感知——加解密逻辑散落进每个 Controller 是维护灾难。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不是所有接口都加密&lt;/strong&gt;。EPG、Meta 这类公开内容接口只走 HTTPS + 签名；登录、充值、播放鉴权、账号操作这些敏感接口才上消息体加密。全量加密的 CPU 成本和排查问题的痛苦不成比例——加密后的日志排障要靠专门的脱密工具，这也是成本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;绝不自研算法&lt;/strong&gt;。所有原语都用成熟实现（AES-GCM、ECDH、HMAC-SHA256），自己拼协议可以，自己造算法不行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;74-必要防御防攻击清单&#34;&gt;7.4 必要防御：防攻击清单&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;攻击面&lt;/th&gt;
					&lt;th&gt;我们的防御手段&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;接口刷量 / CC 攻击&lt;/td&gt;
					&lt;td&gt;网关限流 + Sentinel 熔断；登录、验证码接口按 IP + 账号双维度计数&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;短信/邮件轰炸机&lt;/td&gt;
					&lt;td&gt;验证码存 Redis 带 TTL，&lt;strong&gt;验证成功后立即删除&lt;/strong&gt;防重放；单账号/单 IP 频次限制&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;重放攻击&lt;/td&gt;
					&lt;td&gt;请求带时间戳 + nonce，token 短过期&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;抓包 / 中间人&lt;/td&gt;
					&lt;td&gt;HTTPS + SSL Pinning + 敏感接口消息体加密（见 7.3）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;越权访问&lt;/td&gt;
					&lt;td&gt;tk12 绑定 &lt;code&gt;accountId + streamId&lt;/code&gt;，换账号播放直接鉴权失败&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;篡改网盘包&lt;/td&gt;
					&lt;td&gt;RSA 签名验签，验签不过不解密&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;撞库 / 盗号&lt;/td&gt;
					&lt;td&gt;登录失败计数锁定（&lt;code&gt;account_black_list&lt;/code&gt;）、设备绑定、子账号体系隔离&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;敏感信息外泄&lt;/td&gt;
					&lt;td&gt;交接文档脱敏（我们的交接文档里曾出现明文环境密码，是我抓出来的典型反面教材）；日志脱敏；DB 密码字段只存哈希&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;75-drm内容安全的最后一道门&#34;&gt;7.5 DRM：内容安全的最后一道门&lt;/h3&gt;
&lt;p&gt;DRM 链路的安全设计我同样按零信任思路做：ContentKey 由独立密钥服务（drmencryptor）管理，&lt;strong&gt;先查缓存没有再向外部 DRM 厂商申请并落库缓存&lt;/strong&gt;；播放优先走离线 license（有缓存用缓存），PSSH 异常才降级在线 license。license 本身带过期时间，App 本地不持久化密钥。系统支持 Goose DRM 与 Multi-DRM（Widevine）切换，切换动作就是删旧流加新流——密钥体系与业务流解耦，这个设计让后来的 DRM 厂商迁移几乎没动业务代码。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八可观测性没有度量就没有高可用&#34;&gt;八、可观测性：没有度量就没有高可用&lt;/h2&gt;
&lt;p&gt;我们的现网组合：&lt;strong&gt;ELK 日志 + Skywalking 链路追踪 + Prometheus/Grafana 指标 + Nacos 配置中心&lt;/strong&gt;，每个子系统独立 ES index。&lt;/p&gt;
&lt;p&gt;我压测过的容量数据（v7.16 集群）：只写日志约 11 万条/秒，只写 Skywalking 约 2 万条/秒，混合写时日志 6 万 + 链路 1 万/秒。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;血泪教训&lt;/strong&gt;：周一高峰 Skywalking 曾出现 4 小时写入延迟，日志量增长也让 Logstash 排队。事后我立了两条规矩：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;日志与链路采样分离&lt;/strong&gt;：高峰期链路数据降采样，保住日志；两者绝不能互相拖死。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;本地日志保留 30 天&lt;/strong&gt;，crontab 定时清理——磁盘写满导致的 ELK 雪崩比业务故障还常见。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我还坚持一点：告警要打在&lt;strong&gt;用户可感知的指标&lt;/strong&gt;上（起播成功率、鉴权失败率、链接过期率），而不是只打 CPU 内存——机器好好的但用户看不了，是最尴尬的监控盲区。服务间的依赖关系复杂到一定程度后，「告警来了不知道谁影响了谁」会成为新问题，Netflix 为此专门做过一套服务拓扑系统，我研究过它的实现并整理了笔记：&lt;a href=&#34;https://ftmao.com/blog/blog/posts/netflix-service-topology-at-scale/&#34;&gt;Netflix 服务拓扑系统笔记&lt;/a&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九总结一张-checklist&#34;&gt;九、总结：一张 checklist&lt;/h2&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;我采用的核心手段&lt;/th&gt;
					&lt;th&gt;用到的 Netflix 开源组件 / 实践&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;高并发&lt;/td&gt;
					&lt;td&gt;网关限流、缓存优先、MQ 削峰、多级缓存&lt;/td&gt;
					&lt;td&gt;Zuul 2 异步化思想、EVCache 启发 keCache&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;高可用&lt;/td&gt;
					&lt;td&gt;熔断降级、失败兜底调度器、多租户分库隔离、订阅模式解耦&lt;/td&gt;
					&lt;td&gt;Hystrix（后迁 Resilience4j/Sentinel）、Chaos Monkey 轻量版&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;高性能&lt;/td&gt;
					&lt;td&gt;Meta 全量入内存、ES 检索、CDN/Squid 边缘缓存、大表治理&lt;/td&gt;
					&lt;td&gt;Open Connect 的边缘缓存角色&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;缓存一致性&lt;/td&gt;
					&lt;td&gt;先写 DB 后删缓存 + 延迟双删、逻辑过期防击穿、随机 TTL 防雪崩&lt;/td&gt;
					&lt;td&gt;EVCache 的缓存运营观&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;零信任&lt;/td&gt;
					&lt;td&gt;双票据鉴权、设备绑定、内部接口白名单、最小权限适配层&lt;/td&gt;
					&lt;td&gt;零信任改造思路（mTLS/SPIFFE 评估后暂缓）&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;攻击防御&lt;/td&gt;
					&lt;td&gt;网关 WAF/限流、验证码防重放、RSA 验签、黑名单、敏感接口消息体加密&lt;/td&gt;
					&lt;td&gt;—&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;可观测&lt;/td&gt;
					&lt;td&gt;ELK + Skywalking + Prometheus，日志链路采样分离&lt;/td&gt;
					&lt;td&gt;服务拓扑系统的思路&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;最后一句实话：&lt;strong&gt;这套架构没有一样是发明出来的，全部是踩坑踩出来的&lt;/strong&gt;。网盘密码泄露、Skywalking 四小时延迟、2-3 亿行的历史大表——每个设计决策背后都有一次事故或一次险些事故。Netflix 的开源组件和博客帮我少踩了很多坑，但终究要自己踩过才知道坑有多深。架构师的职责不是画出完美的图，而是让每个坑只踩一次。&lt;/p&gt;
</description>
    </item>
    <item>
      <title>Netflix 实时服务拓扑图笔记：把「谁依赖谁」从考古变成活地图</title>
      <link>https://ftmao.com/blog/posts/netflix-service-topology-map/</link>
      <pubDate>Fri, 29 May 2026 10:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/netflix-service-topology-map/</guid>
      <description>&lt;h1 id=&#34;netflix-实时服务拓扑图笔记把谁依赖谁从考古变成活地图&#34;&gt;Netflix 实时服务拓扑图笔记：把「谁依赖谁」从考古变成活地图&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;原文：&lt;a href=&#34;https://netflixtechblog.com/from-silos-to-service-topology-why-netflix-built-a-real-time-service-map-0165ba13a7bc&#34;&gt;From Silos to Service Topology: Why Netflix Built a Real-Time Service Map&lt;/a&gt;（Netflix Tech Blog，2026-05-29，作者 Parth Jain 等）
目标读者：维护过微服务系统、半夜被告警叫醒过、对着一堆监控面板猜「到底谁挂了」的后端工程师。
阅读目标：搞清楚 Netflix 为什么认为「依赖关系」值得单独立项做一套系统，三路数据源怎么互补，以及那套「网络中间人解析」管线在解决什么问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一问题信号很多地图没有&#34;&gt;一、问题：信号很多，地图没有&lt;/h2&gt;
&lt;p&gt;原文开场是经典的凌晨三点剧本：工程师被 page 醒，某个关键服务错误率升高，用户观影体验受损。在几千个微服务支撑的系统里，这时候要回答的问题其实非常朴素，但谁也答不快。&lt;/p&gt;
&lt;p&gt;Netflix 做了一件值得学的事：&lt;strong&gt;他们没有拍脑袋立项，而是分析了四年间工程师提交的数千条支持请求&lt;/strong&gt;，发现高频问题惊人地一致：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我的上游和下游依赖是谁？&lt;/li&gt;
&lt;li&gt;这个故障是我自己的问题，还是我依赖的某个服务坏了？&lt;/li&gt;
&lt;li&gt;我下线这台服务做维护，会影响谁？该通知哪些团队？&lt;/li&gt;
&lt;li&gt;为什么这个服务在我的指标里显示为「Unknown」？&lt;/li&gt;
&lt;li&gt;最近我的调用链路上有什么变化，能解释这个行为吗？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;全是依赖问题。而传统可观测性三件套各自只给碎片：指标告诉你症状，日志告诉你单个服务在干嘛，追踪告诉你单次请求怎么流的——&lt;strong&gt;没有一样东西能告诉你稳态下整张依赖网长什么样&lt;/strong&gt;。工程师得在凌晨三点用脑子把多个工具的信息缝起来，慢、易错、压力大。&lt;/p&gt;
&lt;p&gt;这里有个判断很准：缺的不是信号，是&lt;strong&gt;关系&lt;/strong&gt;。信号再多，回答不了「谁连着谁」这种结构性问题。&lt;/p&gt;
&lt;h2 id=&#34;二踩过的坑五条教训&#34;&gt;二、踩过的坑：五条教训&lt;/h2&gt;
&lt;p&gt;Netflix 不是第一次干这件事。原文坦承他们试过外部图数据库、商业平台、各种存储和内部原型，每轮迭代换来一条教训：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;实时性是生死线&lt;/strong&gt;：服务每天部署多次的环境里，几小时前的拓扑图是考古资料，不是可观测性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;规模改变一切&lt;/strong&gt;：能装几千节点的方案，到 Netflix 的服务数量和流量规模会撞墙。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;集成是关键&lt;/strong&gt;：不能要求工程师离开现有工作流去学一套新工具。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据质量差比没有更糟&lt;/strong&gt;：错误或不完整的依赖信息会在事故中把人带向错误结论。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单一视角讲不全&lt;/strong&gt;：网络连接数据缺应用上下文，应用指标只覆盖做了埋点的服务，必须多源合并。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这五条几乎条条是钱烧出来的，尤其是第四条——&lt;strong&gt;一个会撒谎的地图比没有地图更害人&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=&#34;三核心设计三个真相来源三张物理隔离的图&#34;&gt;三、核心设计：三个真相来源，三张物理隔离的图&lt;/h2&gt;
&lt;p&gt;这是全文最有分量的一个架构决策。Netflix 的结论是：&lt;strong&gt;没有任何单一数据源能讲完整的依赖故事&lt;/strong&gt;，所以他们用三路互补的数据各自建一张图：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph SRC[&amp;#34;三路数据源&amp;#34;]
        EB[&amp;#34;eBPF 网络流&amp;lt;br/&amp;gt;内核态抓真实连接&amp;#34;]
        IPC[&amp;#34;IPC 指标&amp;lt;br/&amp;gt;应用埋点 gRPC GraphQL REST&amp;#34;]
        TR[&amp;#34;端到端追踪&amp;lt;br/&amp;gt;采样后的真实请求路径&amp;#34;]
    end
    subgraph G[&amp;#34;三张物理隔离的图&amp;#34;]
        G1[&amp;#34;网络图&amp;lt;br/&amp;gt;图数据库分区一&amp;lt;br/&amp;gt;覆盖全 但无应用语义&amp;#34;]
        G2[&amp;#34;应用图&amp;lt;br/&amp;gt;图数据库分区二&amp;lt;br/&amp;gt;有端点细节 但只覆盖已埋点服务&amp;#34;]
        G3[&amp;#34;请求图&amp;lt;br/&amp;gt;列式存储&amp;lt;br/&amp;gt;真实行为 但有采样盲区&amp;#34;]
    end
    EB --&amp;gt; G1
    IPC --&amp;gt; G2
    TR --&amp;gt; G3
    G1 --&amp;gt; U[&amp;#34;统一视图&amp;lt;br/&amp;gt;并行查询三路 合并节点与边&amp;lt;br/&amp;gt;亚秒级返回&amp;#34;]
    G2 --&amp;gt; U
    G3 --&amp;gt; U
    style U fill:#e8f5e9&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三路的价值与短板正好互补：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;eBPF 网络流（网络层）&lt;/strong&gt;：在内核层抓连接记录，&lt;strong&gt;不管应用有没有埋点，每个服务都在图上&lt;/strong&gt;——这是完整性的地基。短板是没有应用上下文：你知道 A 连了 B 的 IP 和协议，但不知道调的是哪个 API 端点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPC 指标（应用层）&lt;/strong&gt;：埋点服务发出的进程间通信指标，能看到具体端点、错误率、延迟分布、协议细节。短板是&lt;strong&gt;只覆盖做了埋点的服务&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;端到端追踪（请求层）&lt;/strong&gt;：不是「A 可以调 B」，而是「A 在处理这个具体请求时确实调了 B」，能捕获条件分支和特性开关带来的真实运行时行为。短板是&lt;strong&gt;采样&lt;/strong&gt;——冷门代码路径在聚合视图里可能看不见。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;工程实现上有个细节值得注意：三张图&lt;strong&gt;物理隔离&lt;/strong&gt;——网络层和 IPC 层各在图数据库的一个分区，追踪层用列式存储（为分析型查询优化）。隔离换来两个好处：各层独立演进，查询时可以三路并行、最后合并结果，即使三层全合并也能做到亚秒返回。&lt;/p&gt;
&lt;p&gt;这个设计翻译过来就是一句话：&lt;strong&gt;完整性靠网络流兜底，语义靠 IPC 补齐，行为真实性靠追踪验证&lt;/strong&gt;。任何单源方案都在这三者里缺一角。&lt;/p&gt;
&lt;h2 id=&#34;四数据管线三级聚合与中间人解析&#34;&gt;四、数据管线：三级聚合与「中间人解析」&lt;/h2&gt;
&lt;p&gt;架构层面，流日志从 Kafka 到可查的图要走这样一条管线：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    K[&amp;#34;多区域 Kafka&amp;lt;br/&amp;gt;持续消费流日志&amp;#34;] --&amp;gt; P[&amp;#34;Pekko Streams&amp;lt;br/&amp;gt;分布式处理 自动分区&amp;lt;br/&amp;gt;天然背压&amp;#34;]
    P --&amp;gt; S1[&amp;#34;第一级 初始聚合&amp;lt;br/&amp;gt;从 Kafka 进来的原始流&amp;#34;]
    S1 --&amp;gt; S2[&amp;#34;第二级 中间人解析&amp;lt;br/&amp;gt;识别 LB NAT 网关 代理&amp;lt;br/&amp;gt;把两跳拼回一跳&amp;#34;]
    S2 --&amp;gt; S3[&amp;#34;第三级 最终聚合&amp;lt;br/&amp;gt;叠加健康状态&amp;#34;]
    S3 --&amp;gt; DB[&amp;#34;图数据库&amp;lt;br/&amp;gt;建在分布式 KV 之上&amp;lt;br/&amp;gt;高吞吐多跳遍历&amp;#34;]
    DB --&amp;gt; API[&amp;#34;gRPC API&amp;lt;br/&amp;gt;多跳遍历 按层过滤&amp;lt;br/&amp;gt;分页 亚秒响应&amp;#34;]
    style S2 fill:#fff3cd&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;三个点值得展开：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;处理框架用 Apache Pekko Streams&lt;/strong&gt;（Akka 的 fork），看重的是分布式容错、自动把负载分区到 Auto Scaling Group、以及响应式流自带的背压处理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三级聚合里最聪明的是第二级&lt;/strong&gt;。网络流日志记录的是逐跳的物理连接：&lt;code&gt;App A → 负载均衡器 → App B&lt;/code&gt;，或者经过 NAT 网关。工程师要的不是这个，是 &lt;code&gt;App A → App B&lt;/code&gt; 这条应用级直连边。第二级负责识别出负载均衡器、NAT 网关、API 网关、代理这些「网络中间人」，把它们的入向流和出向流拼起来，还原出真正的应用到应用路径。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分三级还有个副作用：防热点&lt;/strong&gt;。当某些应用或中间人的流量是其他节点的上百倍时，把聚合分散到多个阶段执行，热点不会压垮单点。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;查询侧是 gRPC API：多跳遍历、按可用性等级（Tier 0、Tier 1 之类）和业务域过滤、大结果集分页、亚秒响应。UI 和 API 并重——&lt;strong&gt;工程师用界面排查，自动化系统（韧性框架、爆炸半径计算器、事故响应机器人）走 API&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id=&#34;五工程师拿到了什么&#34;&gt;五、工程师拿到了什么&lt;/h2&gt;
&lt;p&gt;系统落地后的能力清单，按原文压缩：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;看依赖&lt;/strong&gt;：任意服务的上下游，可按可用性等级和业务域过滤，可切换统一视图或单源视图。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;跳信号&lt;/strong&gt;：从图上任意节点直接跳到对应工具的日志、追踪、指标，不用再手动拼服务名和时间窗。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;算爆炸半径&lt;/strong&gt;：下线维护或大改之前，先看清楚影响谁、通知谁、盯什么。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;叠健康状态&lt;/strong&gt;：拓扑上直接叠加哪些服务正在出问题，快速判断「我看到的故障是不是从别处传过来的」。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;程序化查询&lt;/strong&gt;：Platform Modernization 团队用它验证直播关键服务的整条依赖链都打了正确的可用性等级标记——拓扑成了合规校验的数据源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时间旅行&lt;/strong&gt;：查历史上某个时间点的拓扑长什么样。实现上没有每个时间片存一份（存储会爆炸），而是用&lt;strong&gt;各层专用的聚合器跨时间窗口累积&lt;/strong&gt;，需要历史视图时再重建。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后这条「时间旅行」是我觉得最有味道的：事故排查的黄金问题「&lt;strong&gt;故障开始前，我的依赖图变了什么&lt;/strong&gt;」终于被变成一条查询，而不是靠翻部署记录和记忆。&lt;/p&gt;
&lt;h2 id=&#34;六活的地图和下一步&#34;&gt;六、活的地图，和下一步&lt;/h2&gt;
&lt;p&gt;Netflix 反复强调「living map」这个词：新调用关系出现就近实时上图，调用停止的边会从图里淡出，部署改变行为就改变图。关键承诺是——&lt;strong&gt;工程师可以相信图上画的就是现实，而不是某个人对架构的想象&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;路线图上的三件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;变更事件叠加&lt;/strong&gt;：把部署、配置变更标在拓扑图上，依赖关系和「什么时候改了什么」同框，相关性分析会容易得多。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更丰富的上下文&lt;/strong&gt;：更多端点级细节、协议信息、网络路径信息。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自动根因分析&lt;/strong&gt;：一个智能体持续爬拓扑图、跨依赖关联故障、理解历史模式、自动给出疑似根因——&lt;strong&gt;拓扑图是这类智能自动化的知识图谱地基&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第三条值得划线：他们不是在说「接个 LLM 上去问答」，而是在说「先把依赖关系建成结构化、实时、可查询的图，智能体才有爬的地面」。这个顺序是对的。&lt;/p&gt;
&lt;h2 id=&#34;七工程实践联想&#34;&gt;七、工程实践联想&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;立项之前先做求助记录考古&lt;/strong&gt;。Netflix 拿四年的支持工单说话，高频问题清单就是需求文档。多数内部平台立项靠的是平台团队自己的痛感，而不是用户的——「我觉得你需要」和「数据说你在问」做出来的东西，采纳率天差地别。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「信号够多」不等于「可观测」&lt;/strong&gt;。指标、日志、追踪三件套回答的都是单点或单次的问题，依赖关系这种结构性知识需要单独建模、单独存储。如果你的团队排查故障还在靠「老张知道这条链路」，那就是缺这一层。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;多源互补是处理「讲不全」问题的标准答案&lt;/strong&gt;。任何单一采集手段都有盲区：埋点有覆盖率问题，内核抓包有语义问题，采样有冷门路径问题。与其追求一个完美的源，不如让几个不完美的源互相兜底——这个思路对做 AI Agent 的上下文采集同样成立。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;「中间人解析」是被低估的脏活&lt;/strong&gt;。基础设施里到处是 LB、网关、代理，原始数据永远带着这些噪音。谁能把物理路径还原成逻辑路径，谁的数据才可行动。做链路分析、做 RAG 的数据清洗，都会遇到同构的问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;图建好了，Agent 才有事可做&lt;/strong&gt;。Netflix 把自动根因分析放在路线图的末尾而不是开头，因为智能体的前提是有一张实时、准确、可编程查询的图。这对「AI + 运维」的喧嚣是一盆冷水：先修地基，再谈智能。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;本篇小结&#34;&gt;本篇小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;问题&lt;/strong&gt;：微服务规模下，凌晨三点的工程师回答不了「谁依赖谁、爆炸半径多大、故障源在哪」；四年数千条工单证明这是最痛的缺口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;方案&lt;/strong&gt;：三路数据源各建一图、物理隔离、并行查询后合并——eBPF 网络流保完整性，IPC 指标保应用语义，分布式追踪保真实行为。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;管线&lt;/strong&gt;：多区域 Kafka → Pekko Streams → 三级聚合（核心是第二级的网络中间人解析，把 LB/NAT/代理的两跳还原成应用级直连边）→ 图数据库（建在分布式 KV 上）→ 亚秒级 gRPC API。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;能力&lt;/strong&gt;：依赖可视化、跳转详细信号、爆炸半径、健康叠加、程序化查询、基于时间窗聚合的历史拓扑（时间旅行）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;哲学&lt;/strong&gt;：过时的拓扑图是考古不是可观测性；地图必须反映现实而非设计文档；单一数据源永远讲不全依赖关系。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预告&lt;/strong&gt;：这是系列第一篇，下一篇讲规模化落地的工程硬题——Kafka 消费延迟、GC 停顿、响应式流调试、热点治理。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://netflixtechblog.com/from-silos-to-service-topology-why-netflix-built-a-real-time-service-map-0165ba13a7bc&#34;&gt;From Silos to Service Topology: Why Netflix Built a Real-Time Service Map&lt;/a&gt;（Netflix Technology Blog，2026-05-29，原文）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://ebpf.io/&#34;&gt;eBPF 官方文档&lt;/a&gt;（内核态可编程观测技术，网络层数据源的基础）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://pekko.apache.org/&#34;&gt;Apache Pekko&lt;/a&gt;（Akka 的开源 fork，文中分布式流处理框架）&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://opentelemetry.io/docs/concepts/signals/traces/&#34;&gt;OpenTelemetry Tracing&lt;/a&gt;（分布式追踪的通用概念背景，文中请求层数据源对应物）&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>Spring Boot &#43; Python 双栈架构：AI 服务的工程化实践</title>
      <link>https://ftmao.com/blog/posts/java2python/</link>
      <pubDate>Mon, 20 Apr 2026 14:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/java2python/</guid>
      <description>&lt;h1 id=&#34;spring-boot--python-双栈架构ai-服务的工程化实践&#34;&gt;Spring Boot + Python 双栈架构：AI 服务的工程化实践&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;我开发的方糖猫 AI 视频平台不是一个纯 Python 项目，也不是一个纯 Java 项目。它同时使用了 Spring Boot 3 和 Python 3.11：Java 负责业务系统和 API 服务，Python 负责 AI 推理、渲染和多媒体处理。这篇文章会分享我们为什么要这样做，以及如何解决两个技术栈之间的协作问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一为什么必须双栈&#34;&gt;一、为什么必须双栈？&lt;/h2&gt;
&lt;p&gt;做一个 AI 视频生成平台，需要两类能力：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;能力类型&lt;/th&gt;
					&lt;th&gt;典型需求&lt;/th&gt;
					&lt;th&gt;更适合的栈&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;企业级业务系统&lt;/td&gt;
					&lt;td&gt;用户、订单、权限、支付、任务管理&lt;/td&gt;
					&lt;td&gt;Java / Spring Boot&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;AI 推理与多媒体&lt;/td&gt;
					&lt;td&gt;LLM 调用、Whisper、RAG、Remotion、FFmpeg&lt;/td&gt;
					&lt;td&gt;Python&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Java 的优势在于生态成熟、工程规范、团队熟悉；Python 的优势在于 AI 库丰富、模型推理和多媒体处理工具链完善。&lt;/p&gt;
&lt;p&gt;如果全用 Java，AI 库支持差，很多模型没有 Java SDK；如果全用 Python，业务系统的稳定性、事务处理、监控运维又很难做好。&lt;/p&gt;
&lt;p&gt;所以我们的选择是：&lt;strong&gt;Java 做业务底盘，Python 做 AI 引擎，两者通过清晰的接口协作&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二整体架构&#34;&gt;二、整体架构&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    FE[&amp;#34;前端 (Vue 3)&amp;#34;]
    GW[&amp;#34;API 网关 / BFF (Spring Boot)&amp;lt;br/&amp;gt;用户认证、请求路由、限流、任务状态查询、SSE 推送&amp;#34;]
    BIZ[&amp;#34;业务服务&amp;lt;br/&amp;gt;(Spring Boot)&amp;#34;]
    SCHED[&amp;#34;任务调度服务&amp;lt;br/&amp;gt;(Spring Boot)&amp;#34;]
    RES[&amp;#34;资源管理服务&amp;lt;br/&amp;gt;(Spring Boot)&amp;#34;]
    AI[&amp;#34;AI Worker (Python)&amp;lt;br/&amp;gt;ReAct Agent、RAG 检索、Whisper、TTS、Remotion、FFmpeg&amp;#34;]
    STORE[&amp;#34;存储层：MySQL / Redis / OSS / ChromaDB&amp;#34;]

    FE --&amp;gt; GW
    GW --&amp;gt; BIZ
    GW --&amp;gt; SCHED
    GW --&amp;gt; RES
    SCHED -- &amp;#34;RocketMQ / Kafka&amp;#34; --&amp;gt; AI
    AI --&amp;gt; STORE&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;三职责划分&#34;&gt;三、职责划分&lt;/h2&gt;
&lt;h3 id=&#34;31-java-侧职责&#34;&gt;3.1 Java 侧职责&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API 网关&lt;/strong&gt;：统一入口、鉴权、限流、日志&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户与任务管理&lt;/strong&gt;：用户注册登录、任务创建、任务状态机、历史记录&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;素材元数据管理&lt;/strong&gt;：素材上传、分类、标签、权限&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支付与计费&lt;/strong&gt;（如有）：订单、套餐、用量统计&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任务调度&lt;/strong&gt;：把渲染/生成任务发送到消息队列&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;实时通知&lt;/strong&gt;：SSE 推送任务进度给用户&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;32-python-侧职责&#34;&gt;3.2 Python 侧职责&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AI Agent 编排&lt;/strong&gt;：ReAct 任务规划、Tool 调用&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAG 检索&lt;/strong&gt;：Embedding 计算、向量库查询&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语音识别&lt;/strong&gt;：Whisper 转写&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;语音合成&lt;/strong&gt;：TTS 配音&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;视频渲染&lt;/strong&gt;：Remotion 组件化渲染&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;后处理&lt;/strong&gt;：FFmpeg 转码、压缩、字幕、多分辨率输出&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;33-边界原则&#34;&gt;3.3 边界原则&lt;/h3&gt;
&lt;p&gt;我们定了几条原则来避免职责混乱：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Python 不直接暴露给用户&lt;/strong&gt;。所有用户请求都走 Java API。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python 只做无状态计算&lt;/strong&gt;。任务状态、用户信息、素材元数据都在 Java 侧维护。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通信以异步为主&lt;/strong&gt;。AI 任务通常耗时较长，同步调用容易导致超时和耦合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据以 ID 为纽带&lt;/strong&gt;。Java 和 Python 之间不传大对象，只传任务 ID 和必要参数。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四服务间通信&#34;&gt;四、服务间通信&lt;/h2&gt;
&lt;h3 id=&#34;41-异步通信rocketmq&#34;&gt;4.1 异步通信：RocketMQ&lt;/h3&gt;
&lt;p&gt;AI 生成和渲染任务都走 RocketMQ：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// Java 侧发送任务&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;SendResult result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; rocketMQTemplate.&lt;span style=&#34;color:#a6e22e&#34;&gt;syncSend&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;video-render&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    MessageBuilder.&lt;span style=&#34;color:#a6e22e&#34;&gt;withPayload&lt;/span&gt;(taskMessage).&lt;span style=&#34;color:#a6e22e&#34;&gt;build&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Python 侧消费任务&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;RenderConsumer&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;on_message&lt;/span&gt;(self, msg):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        task &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; json&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;loads(msg&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;body)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        result &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; render_engine&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;process(task)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        self&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;update_task_status(task[&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;task_id&amp;#34;&lt;/span&gt;], &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;completed&amp;#34;&lt;/span&gt;, result)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; ConsumeStatus&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;CONSUME_SUCCESS&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;异步通信的优点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;解耦 Java 和 Python 服务&lt;/li&gt;
&lt;li&gt;Python Worker 可以独立扩缩容&lt;/li&gt;
&lt;li&gt;任务失败可以重试，不阻塞主流程&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;42-同步通信http--grpc&#34;&gt;4.2 同步通信：HTTP / gRPC&lt;/h3&gt;
&lt;p&gt;少数场景需要同步调用，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查询 RAG 素材（用户在前端点击「生成」前希望看到素材推荐）&lt;/li&gt;
&lt;li&gt;获取任务实时状态&lt;/li&gt;
&lt;li&gt;健康检查&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们主要用 HTTP REST，因为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;简单直观，Java 和 Python 生态都完善&lt;/li&gt;
&lt;li&gt;调试方便，curl 即可测试&lt;/li&gt;
&lt;li&gt;对延迟要求不极端&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;gRPC 我们也考虑过，但因为团队对 gRPC 熟悉度不高，且收益不明显，暂时没引入。&lt;/p&gt;
&lt;h3 id=&#34;43-数据一致性&#34;&gt;4.3 数据一致性&lt;/h3&gt;
&lt;p&gt;双栈架构下，任务状态是核心数据。我们用数据库作为唯一真相源：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[&amp;#34;用户提交任务&amp;#34;] --&amp;gt; B[&amp;#34;Java 写入任务记录（status=pending）&amp;#34;]
    B --&amp;gt; C[&amp;#34;发送 RocketMQ 消息&amp;#34;]
    C --&amp;gt; D[&amp;#34;Python Worker 消费&amp;#34;]
    D --&amp;gt; E[&amp;#34;Python 更新任务状态（status=processing/completed/failed）&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;Python Worker 更新数据库时，通过乐观锁防止并发冲突：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;UPDATE&lt;/span&gt; render_task
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;SET&lt;/span&gt; status &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;processing&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    worker_id &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;worker-01&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    updated_at &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; NOW()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;WHERE&lt;/span&gt; task_id &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;xxx&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#66d9ef&#34;&gt;AND&lt;/span&gt; status &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;pending&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;五数据库设计&#34;&gt;五、数据库设计&lt;/h2&gt;
&lt;h3 id=&#34;51-按服务分库还是分表&#34;&gt;5.1 按服务分库还是分表？&lt;/h3&gt;
&lt;p&gt;我们没有完全按微服务分库，而是按业务域分表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;user&lt;/code&gt;：用户信息&lt;/li&gt;
&lt;li&gt;&lt;code&gt;task&lt;/code&gt;：生成任务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;material&lt;/code&gt;：素材元数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;template&lt;/code&gt;：视频模板&lt;/li&gt;
&lt;li&gt;&lt;code&gt;subscription&lt;/code&gt;：订阅/套餐&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所有表都在一个 MySQL 实例里，通过业务 ID 关联。这种方案在业务量不大时性价比最高，未来可按域拆分。&lt;/p&gt;
&lt;h3 id=&#34;52-python-侧如何访问数据库&#34;&gt;5.2 Python 侧如何访问数据库&lt;/h3&gt;
&lt;p&gt;Python 侧需要读取/更新任务状态，我们用的是 SQLAlchemy + 只读/写分离：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; sqlalchemy &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; create_engine
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;from&lt;/span&gt; sqlalchemy.orm &lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; sessionmaker
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;engine &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; create_engine(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;mysql+pymysql://user:pass@host/db&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Session &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; sessionmaker(bind&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;engine)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;update_task_status&lt;/span&gt;(task_id: str, status: str):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;with&lt;/span&gt; Session() &lt;span style=&#34;color:#66d9ef&#34;&gt;as&lt;/span&gt; session:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        session&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;execute(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;UPDATE render_task SET status = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%s&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt; WHERE task_id = &lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;%s&lt;/span&gt;&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            (status, task_id)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        session&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;commit()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;为了降低耦合，Python 侧只更新少量任务状态字段，不碰业务核心表。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六部署与运维&#34;&gt;六、部署与运维&lt;/h2&gt;
&lt;h3 id=&#34;61-容器化&#34;&gt;6.1 容器化&lt;/h3&gt;
&lt;p&gt;两个栈都用 Docker 打包：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Java 服务 Dockerfile&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;FROM&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;eclipse-temurin:17-jdk&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;COPY&lt;/span&gt; target/app.jar /app.jar&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;ENTRYPOINT&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;java&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;-jar&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/app.jar&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;# Python Worker Dockerfile&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;FROM&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;python:3.11-slim&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;WORKDIR&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;/app&lt;/span&gt;&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;COPY&lt;/span&gt; requirements.txt .&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;RUN&lt;/span&gt; pip install -r requirements.txt&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;COPY&lt;/span&gt; . .&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;CMD&lt;/span&gt; [&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;python&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;worker.py&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;62-cicd&#34;&gt;6.2 CI/CD&lt;/h3&gt;
&lt;p&gt;我们用一个简单的 GitHub Actions / Jenkins 流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;代码提交触发构建&lt;/li&gt;
&lt;li&gt;分别构建 Java 镜像和 Python 镜像&lt;/li&gt;
&lt;li&gt;推送到镜像仓库&lt;/li&gt;
&lt;li&gt;生产环境滚动更新&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;63-日志与链路追踪&#34;&gt;6.3 日志与链路追踪&lt;/h3&gt;
&lt;p&gt;双栈环境下，排查问题最大的难点是链路跨语言。我们的做法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有请求携带 &lt;code&gt;trace_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Java 和 Python 都把 &lt;code&gt;trace_id&lt;/code&gt; 打到日志里&lt;/li&gt;
&lt;li&gt;用 ELK / Loki 做日志聚合，按 &lt;code&gt;trace_id&lt;/code&gt; 查询全链路&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; logging
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;logger &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; logging&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;getLogger(__name__)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;logger&lt;span style=&#34;color:#f92672&#34;&gt;.&lt;/span&gt;info(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Start render&amp;#34;&lt;/span&gt;, extra&lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt;{&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;trace_id&amp;#34;&lt;/span&gt;: trace_id, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;task_id&amp;#34;&lt;/span&gt;: task_id})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;64-监控告警&#34;&gt;6.4 监控告警&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;层级&lt;/th&gt;
					&lt;th&gt;监控项&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Java 服务&lt;/td&gt;
					&lt;td&gt;JVM 内存、GC、接口响应时间、错误率&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Python Worker&lt;/td&gt;
					&lt;td&gt;CPU/GPU 利用率、任务处理速率、队列堆积&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;数据库&lt;/td&gt;
					&lt;td&gt;连接数、慢查询&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;消息队列&lt;/td&gt;
					&lt;td&gt;堆积量、消费速率&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七踩过的坑&#34;&gt;七、踩过的坑&lt;/h2&gt;
&lt;h3 id=&#34;71-序列化不一致&#34;&gt;7.1 序列化不一致&lt;/h3&gt;
&lt;p&gt;Java 侧用 Jackson，Python 侧用默认 json，时间格式、枚举值、空值处理不一致，导致接口对接时经常报错。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;统一用 ISO 8601 时间格式&lt;/li&gt;
&lt;li&gt;枚举值用字符串传递&lt;/li&gt;
&lt;li&gt;制定统一的 JSON Schema，用 Pydantic 和 Java Bean Validation 双向校验&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;72-python-服务内存泄漏&#34;&gt;7.2 Python 服务内存泄漏&lt;/h3&gt;
&lt;p&gt;Python Worker 长时间运行后，内存占用越来越高，尤其是加载大模型后。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Worker 处理一定数量任务后自动重启&lt;/li&gt;
&lt;li&gt;模型用单例模式加载，避免重复初始化&lt;/li&gt;
&lt;li&gt;显式清理大对象，调用 &lt;code&gt;gc.collect()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;73-跨语言异常处理&#34;&gt;7.3 跨语言异常处理&lt;/h3&gt;
&lt;p&gt;Python 异常信息传到 Java 侧变成一团乱码，前端无法友好展示。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定义统一的错误码体系&lt;/li&gt;
&lt;li&gt;Python 捕获异常后按规范封装错误响应&lt;/li&gt;
&lt;li&gt;Java 侧根据错误码映射为用户友好的提示&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;74-开发环境复杂度&#34;&gt;7.4 开发环境复杂度&lt;/h3&gt;
&lt;p&gt;开发时需要同时启动 Java 服务、Python Worker、MySQL、Redis、RocketMQ，新成员上手成本高。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;解决&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;提供 &lt;code&gt;docker-compose.yml&lt;/code&gt; 一键启动基础设施&lt;/li&gt;
&lt;li&gt;编写本地开发文档&lt;/li&gt;
&lt;li&gt;支持 Windows 开发环境降级（Python Worker 用 HTTP 模拟 RocketMQ）&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八双栈架构的收益与代价&#34;&gt;八、双栈架构的收益与代价&lt;/h2&gt;
&lt;h3 id=&#34;81-收益&#34;&gt;8.1 收益&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;方面&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;团队分工清晰&lt;/td&gt;
					&lt;td&gt;Java 工程师做业务，Python 工程师做 AI&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;技术选型合理&lt;/td&gt;
					&lt;td&gt;每个栈都用最合适的工具&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;稳定性高&lt;/td&gt;
					&lt;td&gt;业务系统独立演进，AI 故障不直接影响用户注册/支付&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;扩展灵活&lt;/td&gt;
					&lt;td&gt;Python Worker 可以根据渲染负载单独扩容&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;82-代价&#34;&gt;8.2 代价&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;方面&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;运维复杂&lt;/td&gt;
					&lt;td&gt;两套构建、部署、监控体系&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;链路排查难&lt;/td&gt;
					&lt;td&gt;跨语言日志追踪需要额外设计&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;接口契约维护&lt;/td&gt;
					&lt;td&gt;Java 和 Python 之间需要稳定的 API 契约&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九什么情况下不要双栈&#34;&gt;九、什么情况下不要双栈？&lt;/h2&gt;
&lt;p&gt;虽然双栈适合我们，但不是所有项目都需要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;纯业务系统&lt;/strong&gt;：全 Java 即可&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;纯模型研究/实验&lt;/strong&gt;：全 Python 即可&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;团队规模小&lt;/strong&gt;：双栈会增加认知负担&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MVP 早期&lt;/strong&gt;：先用最熟悉的栈跑通，后期再拆分&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们是在产品验证基本跑通、团队有 5 人以上时才明确拆成双栈。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十总结&#34;&gt;十、总结&lt;/h2&gt;
&lt;p&gt;Spring Boot + Python 双栈不是最优架构，而是&lt;strong&gt;适合我们团队能力和业务形态的架构&lt;/strong&gt;。几个关键经验：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;职责边界要清晰&lt;/strong&gt;，否则双栈会变成双倍的混乱。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;异步通信是主力&lt;/strong&gt;，同步只用于低延迟查询场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库是状态的唯一真相源&lt;/strong&gt;，避免两端各自维护状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;日志追踪要跨语言打通&lt;/strong&gt;，否则线上出问题很难排查。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要为双栈而双栈&lt;/strong&gt;，MVP 阶段先用单栈验证。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;如果你也在做 Java + Python 的混合架构，欢迎交流接口契约、部署和监控的经验。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>视频系统 DDD 实战：巴西 IPTV 项目的领域拆分</title>
      <link>https://ftmao.com/blog/posts/iptv/</link>
      <pubDate>Fri, 23 Aug 2024 17:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/iptv/</guid>
      <description>&lt;h1 id=&#34;视频系统-ddd-实战巴西-iptv-项目的领域拆分&#34;&gt;视频系统 DDD 实战：巴西 IPTV 项目的领域拆分&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;2021 年到 2025 年，我负责面向巴西市场的 IPTV 视频点播系统。这是一个用户量过百万、业务持续演进的系统。在几次大的产品迭代中，我们引入了领域驱动设计（DDD）来控制系统复杂度。这篇文章会完整复盘当时的领域拆分过程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一项目背景&#34;&gt;一、项目背景&lt;/h2&gt;
&lt;p&gt;系统核心功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;视频上传：支持 tus / FTP / 网盘 / m3u8 等多种来源&lt;/li&gt;
&lt;li&gt;视频转码：h264 / h265 多码率输出&lt;/li&gt;
&lt;li&gt;DRM 加密：永新 / 自研加密方案&lt;/li&gt;
&lt;li&gt;元数据管理：同步 IMDb / TMDB / PP 等外部数据源&lt;/li&gt;
&lt;li&gt;播放列表：专题、分类、推荐&lt;/li&gt;
&lt;li&gt;播放鉴权：用户权限、设备管理、防盗链&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;服务用户：&lt;strong&gt;200 万+&lt;/strong&gt;，峰值日活 &lt;strong&gt;40 万&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二为什么需要-ddd&#34;&gt;二、为什么需要 DDD？&lt;/h2&gt;
&lt;h3 id=&#34;21-早期的问题&#34;&gt;2.1 早期的问题&lt;/h3&gt;
&lt;p&gt;系统最初是一个单体应用，模块按技术分层组织：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;controller/
service/
dao/
entity/&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这种结构在业务简单时没问题，但随着功能增加，开始出现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个需求改动涉及多个「层」，改哪里不确定&lt;/li&gt;
&lt;li&gt;业务逻辑散落在 Service 中，相似功能重复实现&lt;/li&gt;
&lt;li&gt;转码、鉴权、元数据互相耦合，改一个模块影响多个模块&lt;/li&gt;
&lt;li&gt;新成员上手慢，需要全局理解才敢改代码&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;22-ddd-带来的改变&#34;&gt;2.2 DDD 带来的改变&lt;/h3&gt;
&lt;p&gt;DDD 不是银弹，但它帮我们做了两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;按业务边界组织代码&lt;/strong&gt;，而不是按技术分层&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;显式定义领域概念&lt;/strong&gt;，让业务逻辑有明确的归属&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三领域划分&#34;&gt;三、领域划分&lt;/h2&gt;
&lt;h3 id=&#34;31-核心域识别&#34;&gt;3.1 核心域识别&lt;/h3&gt;
&lt;p&gt;我们先识别出系统的核心业务域：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    content[&amp;#34;内容域 (Content)&amp;lt;br/&amp;gt;媒资管理、视频文件、转码任务、DRM 加密&amp;#34;]
    metadata[&amp;#34;元数据域 (Metadata)&amp;lt;br/&amp;gt;影片信息、演员、分类、标签、外部数据源同步&amp;#34;]
    playback[&amp;#34;播放域 (Playback)&amp;lt;br/&amp;gt;播放鉴权、播放地址生成、防盗链、码率适配&amp;#34;]
    user[&amp;#34;用户域 (User)&amp;lt;br/&amp;gt;用户账户、订阅、设备、播放历史&amp;#34;]
    recommendation[&amp;#34;推荐域 (Recommendation)&amp;lt;br/&amp;gt;播放列表、专题、个性化推荐&amp;#34;]
    billing[&amp;#34;计费域 (Billing)&amp;lt;br/&amp;gt;套餐、订单、支付、用量统计&amp;#34;]
    content --- metadata --- playback --- user --- recommendation --- billing&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;32-域间关系&#34;&gt;3.2 域间关系&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    playback[&amp;#34;播放域&amp;#34;] -- 依赖 --&amp;gt; content[&amp;#34;内容域&amp;#34;]
    playback -- 依赖 --&amp;gt; user[&amp;#34;用户域&amp;#34;]
    recommendation[&amp;#34;推荐域&amp;#34;] -- 依赖 --&amp;gt; metadata[&amp;#34;元数据域&amp;#34;]
    recommendation -- &amp;#34;依赖（获取热门数据）&amp;#34; --&amp;gt; playback
    billing[&amp;#34;计费域&amp;#34;] -- 依赖 --&amp;gt; user
    metadata -- 依赖 --&amp;gt; external[&amp;#34;外部数据源&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;依赖方向尽量保持单向，避免循环依赖。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四内容域详细设计&#34;&gt;四、内容域详细设计&lt;/h2&gt;
&lt;p&gt;内容域是最复杂的域之一，我们以它为例展开。&lt;/p&gt;
&lt;h3 id=&#34;41-聚合根设计&#34;&gt;4.1 聚合根设计&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    root[&amp;#34;MediaAsset（媒资聚合根）&amp;#34;]
    file[&amp;#34;MediaFile（视频文件）&amp;#34;]
    raw[&amp;#34;原始文件&amp;#34;]
    transcoded[&amp;#34;转码后文件（多码率）&amp;#34;]
    drmfile[&amp;#34;DRM 加密文件&amp;#34;]
    job[&amp;#34;TranscodingJob（转码任务）&amp;#34;]
    license[&amp;#34;DrmLicense（DRM 许可证信息）&amp;#34;]
    root --&amp;gt; file
    file --&amp;gt; raw
    file --&amp;gt; transcoded
    file --&amp;gt; drmfile
    root --&amp;gt; job
    root --&amp;gt; license&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;code&gt;MediaAsset&lt;/code&gt; 是聚合根，外部系统只能通过它访问内部实体。&lt;/p&gt;
&lt;h3 id=&#34;42-关键领域事件&#34;&gt;4.2 关键领域事件&lt;/h3&gt;
&lt;p&gt;内容域会产生大量领域事件，其他域可以订阅：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;事件&lt;/th&gt;
					&lt;th&gt;含义&lt;/th&gt;
					&lt;th&gt;订阅方&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;MediaAssetUploaded&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;媒资上传完成&lt;/td&gt;
					&lt;td&gt;转码服务&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;TranscodingCompleted&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;转码完成&lt;/td&gt;
					&lt;td&gt;内容域自身、播放域&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;DrmEncryptionCompleted&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;DRM 加密完成&lt;/td&gt;
					&lt;td&gt;播放域&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;MetadataSynced&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;元数据同步完成&lt;/td&gt;
					&lt;td&gt;推荐域&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;MediaAssetPublished&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;媒资发布上线&lt;/td&gt;
					&lt;td&gt;推荐域、搜索服务&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;43-状态流转&#34;&gt;4.3 状态流转&lt;/h3&gt;
&lt;p&gt;媒资从上传到发布的状态机：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    UPLOADED --&amp;gt; PENDING_TRANSCODE --&amp;gt; TRANSCODING --&amp;gt; PENDING_DRM --&amp;gt; DRM_ENCRYPTED --&amp;gt; PENDING_METADATA --&amp;gt; METADATA_READY --&amp;gt; FINAL[&amp;#34;PUBLISHED / REJECTED&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;每个状态变更都通过领域事件触发下一步操作。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五分层架构&#34;&gt;五、分层架构&lt;/h2&gt;
&lt;p&gt;每个领域内部采用 DDD 经典四层架构：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    ui[&amp;#34;用户接口层&amp;lt;br/&amp;gt;REST Controller / DTO&amp;#34;]
    app[&amp;#34;应用层&amp;lt;br/&amp;gt;Application Service / 用例编排&amp;#34;]
    domain[&amp;#34;领域层&amp;lt;br/&amp;gt;Entity / Value Object / Domain Service / Repository Interface / Domain Event&amp;#34;]
    infra[&amp;#34;基础设施层&amp;lt;br/&amp;gt;Repository Impl / MQ Client / External API Client / DB / Cache&amp;#34;]
    ui --- app --- domain --- infra&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;51-领域层示例&#34;&gt;5.1 领域层示例&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;MediaAsset&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; MediaAssetId id;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; String title;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; MediaStatus status;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; List&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;MediaFile&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; files;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; List&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;TranscodingJob&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; transcodingJobs;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;upload&lt;/span&gt;(MediaFile file) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;files&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;add&lt;/span&gt;(file);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;status&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; MediaStatus.&lt;span style=&#34;color:#a6e22e&#34;&gt;UPLOADED&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        registerEvent(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; MediaAssetUploadedEvent(&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;completeTranscoding&lt;/span&gt;(TranscodingJob job) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;transcodingJobs&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;add&lt;/span&gt;(job);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;status&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; MediaStatus.&lt;span style=&#34;color:#a6e22e&#34;&gt;TRANSCODING_COMPLETED&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        registerEvent(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; TranscodingCompletedEvent(&lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;id&lt;/span&gt;, job.&lt;span style=&#34;color:#a6e22e&#34;&gt;getOutputFiles&lt;/span&gt;()));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;52-防腐层设计&#34;&gt;5.2 防腐层设计&lt;/h3&gt;
&lt;p&gt;与外部系统（IMDb / TMDB / PP）对接时，使用防腐层（Anti-Corruption Layer）：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;MetadataSourceClient&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ExternalMetadata &lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(String externalId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// IMDb 适配器&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ImdbClientAdapter&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;implements&lt;/span&gt; MetadataSourceClient {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;final&lt;/span&gt; ImdbApiClient imdbClient;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; ExternalMetadata &lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(String externalId) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ImdbResponse response &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; imdbClient.&lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(externalId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; MetadataConverter.&lt;span style=&#34;color:#a6e22e&#34;&gt;fromImdb&lt;/span&gt;(response);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;这样外部 API 的数据结构变化不会直接影响到我们内部的元数据模型。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六微服务拆分过程&#34;&gt;六、微服务拆分过程&lt;/h2&gt;
&lt;h3 id=&#34;61-第一次拆分按技术边界&#34;&gt;6.1 第一次拆分：按技术边界&lt;/h3&gt;
&lt;p&gt;早期我们按技术栈拆：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;upload-service&lt;/li&gt;
&lt;li&gt;transcoding-service&lt;/li&gt;
&lt;li&gt;metadata-service&lt;/li&gt;
&lt;li&gt;playback-service&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结果导致一个业务操作要跨多个服务，分布式事务复杂。&lt;/p&gt;
&lt;h3 id=&#34;62-第二次拆分按领域边界&#34;&gt;6.2 第二次拆分：按领域边界&lt;/h3&gt;
&lt;p&gt;调整后按 DDD 领域拆分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;content-service（内容域）&lt;/li&gt;
&lt;li&gt;metadata-service（元数据域）&lt;/li&gt;
&lt;li&gt;playback-service（播放域）&lt;/li&gt;
&lt;li&gt;user-service（用户域）&lt;/li&gt;
&lt;li&gt;recommendation-service（推荐域）&lt;/li&gt;
&lt;li&gt;billing-service（计费域）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每个服务有清晰的业务边界和聚合根。&lt;/p&gt;
&lt;h3 id=&#34;63-服务间通信&#34;&gt;6.3 服务间通信&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;场景&lt;/th&gt;
					&lt;th&gt;通信方式&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;领域事件传播&lt;/td&gt;
					&lt;td&gt;RocketMQ&lt;/td&gt;
					&lt;td&gt;异步、最终一致&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;同步查询&lt;/td&gt;
					&lt;td&gt;HTTP / gRPC&lt;/td&gt;
					&lt;td&gt;强一致查询&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;配置/字典数据&lt;/td&gt;
					&lt;td&gt;Redis 缓存&lt;/td&gt;
					&lt;td&gt;高频读取&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七领域事件实现&#34;&gt;七、领域事件实现&lt;/h2&gt;
&lt;h3 id=&#34;71-事件定义&#34;&gt;7.1 事件定义&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;TranscodingCompletedEvent&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;extends&lt;/span&gt; DomainEvent {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;final&lt;/span&gt; MediaAssetId mediaAssetId;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;final&lt;/span&gt; List&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;FileUrl&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; outputFiles;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;final&lt;/span&gt; Instant completedAt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;72-事件发布&#34;&gt;7.2 事件发布&lt;/h3&gt;
&lt;p&gt;领域层只负责注册事件，应用层统一发布：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Transactional&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;handleTranscodingCallback&lt;/span&gt;(TranscodingJobId jobId, List&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;FileUrl&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; outputs) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    MediaAsset asset &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; mediaAssetRepository.&lt;span style=&#34;color:#a6e22e&#34;&gt;findByJobId&lt;/span&gt;(jobId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    asset.&lt;span style=&#34;color:#a6e22e&#34;&gt;completeTranscoding&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; TranscodingJob(jobId, outputs));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    mediaAssetRepository.&lt;span style=&#34;color:#a6e22e&#34;&gt;save&lt;/span&gt;(asset);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    eventPublisher.&lt;span style=&#34;color:#a6e22e&#34;&gt;publish&lt;/span&gt;(asset.&lt;span style=&#34;color:#a6e22e&#34;&gt;getDomainEvents&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    asset.&lt;span style=&#34;color:#a6e22e&#34;&gt;clearDomainEvents&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;h3 id=&#34;73-事件消费&#34;&gt;7.3 事件消费&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;@Component&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;PlaybackEventListener&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a6e22e&#34;&gt;@RocketMQMessageListener&lt;/span&gt;(topic &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;content-events&amp;#34;&lt;/span&gt;, selectorExpression &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;TranscodingCompleted&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Listener&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;implements&lt;/span&gt; RocketMQListener&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;TranscodingCompletedEvent&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a6e22e&#34;&gt;@Override&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;onMessage&lt;/span&gt;(TranscodingCompletedEvent event) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            playbackService.&lt;span style=&#34;color:#a6e22e&#34;&gt;preparePlayUrls&lt;/span&gt;(event.&lt;span style=&#34;color:#a6e22e&#34;&gt;getMediaAssetId&lt;/span&gt;(), event.&lt;span style=&#34;color:#a6e22e&#34;&gt;getOutputFiles&lt;/span&gt;());
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;hr&gt;
&lt;h2 id=&#34;八几次关键演进&#34;&gt;八、几次关键演进&lt;/h2&gt;
&lt;h3 id=&#34;81-从单体到领域化单体&#34;&gt;8.1 从单体到领域化单体&lt;/h3&gt;
&lt;p&gt;第一步不是拆微服务，而是先在单体内部按领域组织代码包：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;com.ftmao.iptv.content
com.ftmao.iptv.metadata
com.ftmao.iptv.playback&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这样可以在不引入分布式复杂度的前提下，先验证领域划分是否合理。&lt;/p&gt;
&lt;h3 id=&#34;82-从同步调用到事件驱动&#34;&gt;8.2 从同步调用到事件驱动&lt;/h3&gt;
&lt;p&gt;转码完成后，原本播放域通过接口轮询内容域查询状态。后来改为内容域发布 &lt;code&gt;TranscodingCompleted&lt;/code&gt; 事件，播放域订阅。&lt;/p&gt;
&lt;p&gt;收益：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内容域和播放域解耦&lt;/li&gt;
&lt;li&gt;新增订阅方不需要改内容域&lt;/li&gt;
&lt;li&gt;系统扩展性更好&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;83-引入-cqrs-处理播放查询&#34;&gt;8.3 引入 CQRS 处理播放查询&lt;/h3&gt;
&lt;p&gt;播放接口对性能要求很高，我们单独做了读模型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写模型：MediaAsset 聚合根&lt;/li&gt;
&lt;li&gt;读模型：Redis 中的播放信息缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当内容发布或更新时，通过事件同步更新读模型。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九踩过的坑&#34;&gt;九、踩过的坑&lt;/h2&gt;
&lt;h3 id=&#34;91-领域划分过细&#34;&gt;9.1 领域划分过细&lt;/h3&gt;
&lt;p&gt;早期我们划分了 10 多个领域，导致服务数量爆炸，运维成本剧增。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：领域数量要匹配团队规模。小团队 4-6 个领域足够。&lt;/p&gt;
&lt;h3 id=&#34;92-领域事件丢失&#34;&gt;9.2 领域事件丢失&lt;/h3&gt;
&lt;p&gt;消息队列配置不当，出现过领域事件丢失，导致播放域看不到新发布的内容。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：关键领域事件必须持久化，支持重放和补偿。&lt;/p&gt;
&lt;h3 id=&#34;93-贫血领域模型&#34;&gt;9.3 贫血领域模型&lt;/h3&gt;
&lt;p&gt;很多「DDD」项目最后变成了 Transaction Script 套壳，实体只有 getter/setter。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：要把业务规则和状态变更放在领域对象里，而不是 Service 里。&lt;/p&gt;
&lt;h3 id=&#34;94-过度设计聚合根&#34;&gt;9.4 过度设计聚合根&lt;/h3&gt;
&lt;p&gt;一开始想把所有内容相关的东西都放到 &lt;code&gt;MediaAsset&lt;/code&gt; 聚合根里，结果聚合根过大，事务边界混乱。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;教训&lt;/strong&gt;：聚合根要小而聚焦，跨聚合用领域事件协作。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十效果与反思&#34;&gt;十、效果与反思&lt;/h2&gt;
&lt;h3 id=&#34;101-量化效果&#34;&gt;10.1 量化效果&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;指标&lt;/th&gt;
					&lt;th&gt;优化前&lt;/th&gt;
					&lt;th&gt;优化后&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;新需求平均改动文件数&lt;/td&gt;
					&lt;td&gt;40 个&lt;/td&gt;
					&lt;td&gt;20 个&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;线上因耦合导致的 Bug&lt;/td&gt;
					&lt;td&gt;10 次/月&lt;/td&gt;
					&lt;td&gt;2 次/月&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;新成员独立上手时间&lt;/td&gt;
					&lt;td&gt;3 周&lt;/td&gt;
					&lt;td&gt;1 周&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;102-核心反思&#34;&gt;10.2 核心反思&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;DDD 不是先拆分微服务，而是先拆清楚业务边界&lt;/strong&gt;。微服务是结果，不是起点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;领域事件是域间协作的核心&lt;/strong&gt;，但事件丢失和顺序问题必须重视。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要追求完美的领域模型&lt;/strong&gt;，先有一个可用的模型，再迭代优化。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DDD 需要团队共识&lt;/strong&gt;，不是架构师一个人画几张图就行的。&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十一总结&#34;&gt;十一、总结&lt;/h2&gt;
&lt;p&gt;巴西 IPTV 项目让我对 DDD 有了真实的体感。它不是教科书上的概念，而是解决复杂业务系统演化问题的工程方法。&lt;/p&gt;
&lt;p&gt;对于视频系统这类业务复杂、流程长、多团队协作的系统，DDD 能带来的最大价值是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;让每个业务概念都有清晰的归属&lt;/li&gt;
&lt;li&gt;让系统变更的影响范围可控&lt;/li&gt;
&lt;li&gt;让新成员能更快理解业务&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;如果你也在做 DDD 实践，欢迎在评论区交流领域划分和事件驱动的经验。&lt;/em&gt;&lt;/p&gt;
</description>
    </item>
    <item>
      <title>《领域驱动设计》精简版：一张图看懂 DDD，引用开源 ddd-demo</title>
      <link>https://ftmao.com/blog/posts/domain-driven-design-concise/</link>
      <pubDate>Thu, 27 Jul 2023 20:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/posts/domain-driven-design-concise/</guid>
      <description>&lt;h1 id=&#34;领域驱动设计精简版一张图看懂-ddd引用开源-ddd-demo&#34;&gt;《领域驱动设计》精简版：一张图看懂 DDD，引用开源 ddd-demo&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;这份笔记基于 Eric Evans 的《领域驱动设计》以及 Vaughn Vernon 的《实现领域驱动设计》《领域驱动设计精粹》，结合日常工程实践做了补充。文中完整代码实现参考开源项目 &lt;a href=&#34;https://gitee.com/soulnew/ddd-demo&#34;&gt;soulnew/ddd-demo&lt;/a&gt;，可直接克隆运行。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;一为什么需要-ddd&#34;&gt;一、为什么需要 DDD？&lt;/h2&gt;
&lt;p&gt;传统分层架构（Controller → Service → DAO）的问题：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;问题&lt;/th&gt;
					&lt;th&gt;表现&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;贫血模型&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;Service&lt;/code&gt; 里全是 if/else，Entity 只有 getter/setter&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;语义泄漏&lt;/td&gt;
					&lt;td&gt;业务术语被翻译成 &lt;code&gt;status = 1&lt;/code&gt;，新人读不懂&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;边界模糊&lt;/td&gt;
					&lt;td&gt;一个 &lt;code&gt;OrderService&lt;/code&gt; 同时处理库存、支付、物流&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;复用困难&lt;/td&gt;
					&lt;td&gt;同样叫“用户”，在会员、权限、订单里含义不同&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;DDD 的核心主张：&lt;strong&gt;让软件结构对齐业务领域，用业务语言写代码，用边界保护领域模型&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;二一张图ddd-战略-战术全景&#34;&gt;二、一张图：DDD 战略-战术全景&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;mindmap
  root((领域驱动设计 DDD))
    战略设计 Strategic Design
      领域 Domain
      子域 Subdomain
        核心域 Core
        支撑域 Supporting
        通用域 Generic
      限界上下文 Bounded Context
      上下文映射 Context Mapping
        合作关系 Partnership
        共享内核 Shared Kernel
        客户-供应商 Customer-Supplier
        遵奉者 Conformist
        防腐层 ACL
        开放主机服务 OHS
        发布语言 PL
        各行其道 Separate Ways
    战术设计 Tactical Design
      实体 Entity
      值对象 Value Object
      聚合 Aggregate
      领域服务 Domain Service
      仓储 Repository
      工厂 Factory
      领域事件 Domain Event
      应用服务 Application Service
    统一语言 Ubiquitous Language
    分层架构 Layered Architecture
      用户界面层 UI
      应用层 Application
      领域层 Domain
      基础设施层 Infrastructure&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;记忆口诀&lt;/strong&gt;：先战略（划边界），后战术（建模型），全程讲“统一语言”。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;三战略设计先画地图再打仗&#34;&gt;三、战略设计：先画地图，再打仗&lt;/h2&gt;
&lt;h3 id=&#34;31-领域--问题空间--解决方案空间&#34;&gt;3.1 领域 = 问题空间 + 解决方案空间&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[问题空间&amp;lt;br/&amp;gt;Problem Space] --&amp;gt; B[领域 Domain&amp;lt;br/&amp;gt;业务要解决的问题]
    B --&amp;gt; C[子域 Subdomain&amp;lt;br/&amp;gt;可独立讨论的业务板块]
    C --&amp;gt; D[核心域 Core Domain&amp;lt;br/&amp;gt;竞争优势]
    C --&amp;gt; E[支撑域 Supporting&amp;lt;br/&amp;gt;定制但非核心]
    C --&amp;gt; F[通用域 Generic&amp;lt;br/&amp;gt;可外购/复用]
    D --&amp;gt; G[解决方案空间&amp;lt;br/&amp;gt;Solution Space]
    E --&amp;gt; G
    F --&amp;gt; G
    G --&amp;gt; H[限界上下文 Bounded Context&amp;lt;br/&amp;gt;一个模型只在一个上下文内有效]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;示例：在线医疗平台&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;子域&lt;/th&gt;
					&lt;th&gt;类型&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;问诊&lt;/td&gt;
					&lt;td&gt;核心域&lt;/td&gt;
					&lt;td&gt;核心竞争力，需精研模型&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;处方&lt;/td&gt;
					&lt;td&gt;支撑域&lt;/td&gt;
					&lt;td&gt;受监管约束，需定制但非差异化&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;支付&lt;/td&gt;
					&lt;td&gt;通用域&lt;/td&gt;
					&lt;td&gt;可直接对接微信/支付宝&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;消息通知&lt;/td&gt;
					&lt;td&gt;通用域&lt;/td&gt;
					&lt;td&gt;短信/推送可用第三方&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;32-限界上下文-bounded-context&#34;&gt;3.2 限界上下文 Bounded Context&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;定义&lt;/strong&gt;：限界上下文是领域模型能够保持一致性和完整性的显式边界。在边界内部，每个业务术语、概念、规则都有精确且唯一的含义；同一概念在不同限界上下文中可以拥有不同定义。它是 DDD 战略设计中保护模型不被语义污染的最小单元。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;同一个业务名词，在不同上下文里含义可能完全不同。DDD 用“限界上下文”给模型画边界。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    subgraph 商品上下文 Product Context
        P1[商品 = SKU + 价格 + 库存]
    end
    subgraph 订单上下文 Order Context
        P2[商品 = 快照 + 购买时价格 + 数量]
    end
    subgraph 物流上下文 Logistics Context
        P3[商品 = 重量 + 体积 + 易碎标识]
    end
    subgraph 营销上下文 Marketing Context
        P4[商品 = 卖点 + 优惠券适用规则]
    end
    P1 -.-&amp;gt;|映射| P2
    P1 -.-&amp;gt;|映射| P3
    P1 -.-&amp;gt;|映射| P4&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;关键认知&lt;/strong&gt;：不要追求“一个全局统一的数据模型”，要在上下文内部统一，在上下文之间显式映射。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;33-上下文映射模式&#34;&gt;3.3 上下文映射模式&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;定义&lt;/strong&gt;：上下文映射模式是 DDD 中用于描述不同限界上下文之间集成关系与协作方式的战略模式集合。由于每个限界上下文都有自己的模型和统一语言，当业务流程跨越多个上下文时，需要通过上下文映射来显式定义它们如何依赖、转换与协作，从而保证边界清晰、演化可控。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[上下文 A] --&amp;gt;|合作关系&amp;lt;br/&amp;gt;Partnership| B[上下文 B]
    C[上下文 C] --&amp;gt;|共享内核&amp;lt;br/&amp;gt;Shared Kernel| D[上下文 D]
    E[上游 Upstream] --&amp;gt;|客户-供应商&amp;lt;br/&amp;gt;Customer-Supplier| F[下游 Downstream]
    G[强势上游] --&amp;gt;|遵奉者&amp;lt;br/&amp;gt;Conformist| H[下游]
    I[下游] --&amp;gt;|防腐层 ACL| J[外部系统]
    K[开放主机服务 OHS&amp;lt;br/&amp;gt;+ 发布语言 PL] --&amp;gt; L[下游]
    M[上下文 M] -.-&amp;gt;|各行其道&amp;lt;br/&amp;gt;Separate Ways| N[上下文 N]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;模式&lt;/th&gt;
					&lt;th&gt;使用场景&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;合作关系&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;两个团队互相依赖，共同演进&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;共享内核&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;核心模型由双方共同维护，改动需同步&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;客户-供应商&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;上游优先服务下游需求，下游有话语权&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;遵奉者&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;上游强势（如外部 SaaS），下游无条件接受&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;防腐层 ACL&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;隔离遗留系统或外部 API，保护领域模型&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;开放主机服务 + 发布语言&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;上游提供稳定 API/协议，供多个下游消费&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;各行其道&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;关联微弱，各自独立&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;34-从限界上下文到微服务边界&#34;&gt;3.4 从限界上下文到微服务边界&lt;/h2&gt;
&lt;p&gt;限界上下文是&lt;strong&gt;语义边界&lt;/strong&gt;——一个模型在这个边界内含义一致；微服务是&lt;strong&gt;部署边界&lt;/strong&gt;——独立代码库、独立数据库、独立发布单元。二者不是一回事，但 DDD 划分微服务边界时，最可靠的起点就是限界上下文。&lt;/p&gt;
&lt;h3 id=&#34;映射原则&#34;&gt;映射原则&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[业务领域] --&amp;gt; B[识别子域&amp;lt;br/&amp;gt;Core / Supporting / Generic]
    B --&amp;gt; C[定义限界上下文&amp;lt;br/&amp;gt;语义边界]
    C --&amp;gt; D{是否满足&amp;lt;br/&amp;gt;拆分条件？}
    D --&amp;gt;|是| E[一个微服务]
    D --&amp;gt;|否| F[多个上下文合并&amp;lt;br/&amp;gt;或模块化单体]
    E --&amp;gt; G[独立数据库&amp;lt;br/&amp;gt;独立部署&amp;lt;br/&amp;gt;API / 事件交互]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;默认规则&lt;/strong&gt;：一个限界上下文对应一个微服务。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;例外情况&lt;/strong&gt;：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;情况&lt;/th&gt;
					&lt;th&gt;处理&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;上下文内部模块差异极大（性能、安全、团队）&lt;/td&gt;
					&lt;td&gt;可进一步拆分，但要保留相同的“统一语言”子空间&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;多个上下文非常小且高度协作&lt;/td&gt;
					&lt;td&gt;可合并为一个服务，内部用模块隔离&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;团队规模小、业务简单&lt;/td&gt;
					&lt;td&gt;先模块化单体，随演进再拆&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;强一致事务需求&lt;/td&gt;
					&lt;td&gt;优先放在同一服务 / 同一数据库内&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;拆分的启发条件&#34;&gt;拆分的启发条件&lt;/h3&gt;
&lt;p&gt;满足以下任意一条，都可以把限界上下文拆成独立微服务：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;独立演进&lt;/strong&gt;：团队能独立发布，不阻塞他人。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;独立伸缩&lt;/strong&gt;：流量特征不同，例如订单读多写少，库存强一致写频繁。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;独立故障域&lt;/strong&gt;：一个服务故障不应拖垮整个平台。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;独立技术栈&lt;/strong&gt;：某些上下文需要特殊存储（图数据库、时序数据库等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;合规隔离&lt;/strong&gt;：支付、医疗、金融数据需要独立部署和审计。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;案例b2c-电商的微服务边界划分&#34;&gt;案例：B2C 电商的微服务边界划分&lt;/h3&gt;
&lt;p&gt;以常见的 B2C 电商为例，演示“子域 → 限界上下文 → 微服务”的完整推导过程。&lt;/p&gt;
&lt;h4 id=&#34;第一步识别子域&#34;&gt;第一步：识别子域&lt;/h4&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;子域&lt;/th&gt;
					&lt;th&gt;类型&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;订单&lt;/td&gt;
					&lt;td&gt;核心域&lt;/td&gt;
					&lt;td&gt;下单、状态机、优惠计算&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;库存&lt;/td&gt;
					&lt;td&gt;核心域&lt;/td&gt;
					&lt;td&gt;实时库存、预占、扣减&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;支付&lt;/td&gt;
					&lt;td&gt;通用域&lt;/td&gt;
					&lt;td&gt;对接微信 / 支付宝 / 信用卡&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;物流&lt;/td&gt;
					&lt;td&gt;支撑域&lt;/td&gt;
					&lt;td&gt;运单、轨迹、配送规则&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;商品&lt;/td&gt;
					&lt;td&gt;核心域&lt;/td&gt;
					&lt;td&gt;类目、属性、价格&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;营销&lt;/td&gt;
					&lt;td&gt;支撑域&lt;/td&gt;
					&lt;td&gt;优惠券、满减、秒杀&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;会员&lt;/td&gt;
					&lt;td&gt;支撑域&lt;/td&gt;
					&lt;td&gt;积分、等级、权益&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&#34;第二步定义限界上下文与统一语言&#34;&gt;第二步：定义限界上下文与统一语言&lt;/h4&gt;
&lt;p&gt;同一个业务名词在不同上下文里含义不同，这正是拆分的语义依据。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;上下文&lt;/th&gt;
					&lt;th&gt;“商品”在该上下文中的含义&lt;/th&gt;
					&lt;th&gt;“订单”在该上下文中的含义&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;商品上下文&lt;/td&gt;
					&lt;td&gt;SKU、SPU、类目、主图、售价&lt;/td&gt;
					&lt;td&gt;无&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;订单上下文&lt;/td&gt;
					&lt;td&gt;商品快照（下单时的价格和描述）&lt;/td&gt;
					&lt;td&gt;聚合根：状态、金额、行项&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;库存上下文&lt;/td&gt;
					&lt;td&gt;可售数量、预占数量、仓库&lt;/td&gt;
					&lt;td&gt;无&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;物流上下文&lt;/td&gt;
					&lt;td&gt;重量、体积、易碎标识&lt;/td&gt;
					&lt;td&gt;发货单、包裹、运单&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;营销上下文&lt;/td&gt;
					&lt;td&gt;适用优惠券的商品范围&lt;/td&gt;
					&lt;td&gt;优惠分摊、实付金额&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;关键判断&lt;/strong&gt;：如果两个地方对“商品”的定义和生命周期完全不同，就应分属不同上下文，也倾向于拆成不同微服务。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&#34;第三步映射为微服务&#34;&gt;第三步：映射为微服务&lt;/h4&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    subgraph 微服务边界
        P[商品服务&amp;lt;br/&amp;gt;Product Service]
        O[订单服务&amp;lt;br/&amp;gt;Order Service]
        I[库存服务&amp;lt;br/&amp;gt;Inventory Service]
        PAY[支付服务&amp;lt;br/&amp;gt;Payment Service]
        L[物流服务&amp;lt;br/&amp;gt;Logistics Service]
        M[营销服务&amp;lt;br/&amp;gt;Marketing Service]
        U[会员服务&amp;lt;br/&amp;gt;Member Service]
    end

    O --&amp;gt;|预占库存| I
    O --&amp;gt;|发起支付| PAY
    I --&amp;gt;|OrderPaid 事件| L
    O --&amp;gt;|查询商品信息| P
    O --&amp;gt;|计算优惠| M
    U --&amp;gt;|积分抵扣| O&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h4 id=&#34;第四步选择上下文映射模式&#34;&gt;第四步：选择上下文映射模式&lt;/h4&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;关系&lt;/th&gt;
					&lt;th&gt;模式&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;订单 → 库存&lt;/td&gt;
					&lt;td&gt;客户-供应商 + 最终一致&lt;/td&gt;
					&lt;td&gt;订单调用库存预占接口；库存发布 &lt;code&gt;InventoryReserved&lt;/code&gt; 事件&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;订单 → 支付&lt;/td&gt;
					&lt;td&gt;开放主机服务 OHS&lt;/td&gt;
					&lt;td&gt;支付提供标准支付网关 API，多个业务共用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;库存 → 物流&lt;/td&gt;
					&lt;td&gt;发布语言 PL + 领域事件&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;OrderPaid&lt;/code&gt; 事件触发物流创建发货单&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;订单 → 商品&lt;/td&gt;
					&lt;td&gt;遵奉者 / ACL&lt;/td&gt;
					&lt;td&gt;商品模型由商品服务定义，订单只保存快照&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;营销 → 订单&lt;/td&gt;
					&lt;td&gt;共享内核 Shared Kernel&lt;/td&gt;
					&lt;td&gt;优惠分摊算法需双方共同维护&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&#34;常见反模式&#34;&gt;常见反模式&lt;/h4&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;反模式&lt;/th&gt;
					&lt;th&gt;问题&lt;/th&gt;
					&lt;th&gt;正确做法&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;按 CRUD 实体拆分&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;UserService&lt;/code&gt;、&lt;code&gt;OrderService&lt;/code&gt;、&lt;code&gt;ProductService&lt;/code&gt; 只是数据表代理&lt;/td&gt;
					&lt;td&gt;按业务能力（上下文）拆分，服务内包含完整业务规则&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;按前端页面拆分&lt;/td&gt;
					&lt;td&gt;一个页面一个服务&lt;/td&gt;
					&lt;td&gt;页面是组合层，服务应按领域聚合&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;过度拆分&lt;/td&gt;
					&lt;td&gt;一次下单调 20 个接口&lt;/td&gt;
					&lt;td&gt;先模块化单体，验证边界后再拆&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;共享数据库&lt;/td&gt;
					&lt;td&gt;多个服务直接操作同一张表&lt;/td&gt;
					&lt;td&gt;每个服务拥有自己的数据，通过 API / 事件交互&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一句话&lt;/strong&gt;：微服务边界不是拍脑袋定的，而是从“限界上下文”出发，结合团队、性能、合规、发布节奏再微调的结果。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;四战术设计在限界上下文里建模型&#34;&gt;四、战术设计：在限界上下文里建模型&lt;/h2&gt;
&lt;h3 id=&#34;41-核心概念关系图&#34;&gt;4.1 核心概念关系图&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;classDiagram
    class AggregateRoot {
        +Entity root
        +invariant()
    }
    class Entity {
        +identity
        +continuity
    }
    class ValueObject {
        +immutable
        +equality by value
    }
    class DomainEvent {
        +eventId
        +occurredOn
        +payload
    }
    class Repository {
        +save(aggregate)
        +byId(id)
    }
    class DomainService {
        +cross-aggregate logic
    }
    class Factory {
        +create()
    }
    AggregateRoot --&amp;gt; Entity : contains
    AggregateRoot --&amp;gt; ValueObject : contains
    AggregateRoot --&amp;gt; DomainEvent : publishes
    Repository --&amp;gt; AggregateRoot : persists
    Factory --&amp;gt; AggregateRoot : creates
    DomainService --&amp;gt; AggregateRoot : orchestrates&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;42-实体-entity-vs-值对象-value-object&#34;&gt;4.2 实体 Entity vs 值对象 Value Object&lt;/h3&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    subgraph Entity
        E1[Order #1001] --&amp;gt; E2[状态变化&amp;lt;br/&amp;gt;待支付 → 已发货 → 已完成]
        E2 --&amp;gt; E3[同一 identity&amp;lt;br/&amp;gt;状态变了还是它]
    end
    subgraph Value Object
        V1[Money amount=100 currency=CNY] --&amp;gt; V2[不可变]
        V2 --&amp;gt; V3[替换即新生&amp;lt;br/&amp;gt;money = money * 2]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;判断标准&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果对象需要被&lt;strong&gt;追踪生命周期&lt;/strong&gt;，有独立身份 → &lt;strong&gt;实体&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;如果对象只描述属性，换了值就等于换了对象 → &lt;strong&gt;值对象&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;五完整设计-demo电商订单上下文&#34;&gt;五、完整设计 Demo：电商订单上下文&lt;/h2&gt;
&lt;p&gt;完整的代码实现可参考开源项目：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gitee: soulnew/ddd-demo&lt;/strong&gt;&lt;br&gt;
&lt;a href=&#34;https://gitee.com/soulnew/ddd-demo&#34;&gt;https://gitee.com/soulnew/ddd-demo&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;该项目演示了一个典型的 DDD 工程结构，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;领域层&lt;/strong&gt;：实体、值对象、聚合根、领域服务、领域事件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用层&lt;/strong&gt;：应用服务、Command/Query、事务编排&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;基础设施层&lt;/strong&gt;：仓储实现、外部 API 适配、消息发布&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接口层&lt;/strong&gt;：Controller / API 入口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;建议结合本节的战术概念，重点阅读：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;domain/&lt;/code&gt; 下的聚合根如何封装业务规则（如订单状态机）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;application/&lt;/code&gt; 下的应用服务如何编排用例，不写业务规则。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;infrastructure/&lt;/code&gt; 下的仓储实现如何把领域模型映射到持久化。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你想快速跑通一个最小可运行示例，也可以直接克隆该项目：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;git clone https://gitee.com/soulnew/ddd-demo.git&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;下面只保留一个最小示意图，帮助理解“充血模型”与“贫血模型”的关键差异。&lt;/p&gt;
&lt;h3 id=&#34;51-充血模型-vs-贫血模型对比&#34;&gt;5.1 充血模型 vs 贫血模型对比&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;贫血模型&lt;/th&gt;
					&lt;th&gt;充血模型&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;OrderService&lt;/code&gt; 里写 &lt;code&gt;if (status.equals(&amp;quot;PAID&amp;quot;))&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;Order.pay()&lt;/code&gt; 自己封装状态机&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;OrderEntity&lt;/code&gt; 只有 getter/setter&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;Order&lt;/code&gt; 有业务方法，规则集中在领域对象&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;规则散落在 Service&lt;/td&gt;
					&lt;td&gt;不变量、状态变更由聚合根保证&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;持久化即领域模型&lt;/td&gt;
					&lt;td&gt;持久化是领域模型的投影&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;核心原则&lt;/strong&gt;：把“什么样合法”的判断放到领域对象里，应用服务只负责“什么时候调用”。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;六分层架构代码放哪里&#34;&gt;六、分层架构：代码放哪里？&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    subgraph 用户界面层 UI/API
        U1[Controller / Handler]
    end
    subgraph 应用层 Application
        A1[Application Service]
        A2[DTO / Command / Query]
    end
    subgraph 领域层 Domain
        D1[Entity]
        D2[Value Object]
        D3[Domain Service]
        D4[Domain Event]
        D5[Repository Interface]
    end
    subgraph 基础设施层 Infrastructure
        I1[Repository Implementation]
        I2[Message Queue]
        I3[Database]
        I4[External API Client]
    end

    U1 --&amp;gt; A1
    A1 --&amp;gt; D1
    A1 --&amp;gt; D3
    D1 --&amp;gt; D2
    D1 -.-&amp;gt;|发布| D4
    A1 --&amp;gt; D5
    D5 -.-&amp;gt;|由| I1
    I1 --&amp;gt; I3
    A1 -.-&amp;gt;|发送| I2
    A1 -.-&amp;gt;|调用| I4&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;依赖方向&lt;/strong&gt;：UI → 应用 → 领域；基础设施通过接口被领域依赖（依赖倒置）。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;层级&lt;/th&gt;
					&lt;th&gt;职责&lt;/th&gt;
					&lt;th&gt;不该做的事&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;UI&lt;/td&gt;
					&lt;td&gt;接收请求、返回响应&lt;/td&gt;
					&lt;td&gt;不直接操作 Repository&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;应用层&lt;/td&gt;
					&lt;td&gt;编排用例、事务、权限校验&lt;/td&gt;
					&lt;td&gt;不写业务规则&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;领域层&lt;/td&gt;
					&lt;td&gt;业务规则、不变量、状态变更&lt;/td&gt;
					&lt;td&gt;不依赖框架/数据库&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;基础设施&lt;/td&gt;
					&lt;td&gt;持久化、消息、外部调用&lt;/td&gt;
					&lt;td&gt;不侵入领域规则&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;七聚合设计四条黄金规则&#34;&gt;七、聚合设计：四条黄金规则&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph TB
    subgraph 聚合根 Order
        O1[Order] --&amp;gt; L1[OrderLineItem]
        O1 --&amp;gt; L2[OrderLineItem]
        O1 --&amp;gt; M1[Money total]
        O1 --&amp;gt; A1[Address shipping]
        O1 --&amp;gt; E1[DomainEvent list]
    end
    subgraph 外部只能通过聚合根访问
        X[其他上下文] --&amp;gt;|byId/order.pay| O1
        X -.-&amp;gt;|❌ 不能直接修改| L1
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;一个聚合只有一个聚合根&lt;/strong&gt;，外部对象只能引用聚合根 ID。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;聚合内部强一致&lt;/strong&gt;，跨聚合最终一致（用领域事件）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;小聚合优先&lt;/strong&gt;，大聚合会成性能瓶颈和并发热点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;删除聚合根级联删除其内部对象&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;反例&lt;/strong&gt;：如果把 &lt;code&gt;User&lt;/code&gt; 和 &lt;code&gt;Order&lt;/code&gt; 放在同一个聚合里，用户每次下单都会锁定整个用户聚合，极端不合理。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;八领域事件跨聚合上下文的胶水&#34;&gt;八、领域事件：跨聚合/上下文的胶水&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;sequenceDiagram
    participant O as Order 聚合
    participant E as Event Bus
    participant I as Inventory 上下文
    participant L as Logistics 上下文
    O-&amp;gt;&amp;gt;O: order.pay()
    O-&amp;gt;&amp;gt;E: publish OrderPaid
    E-&amp;gt;&amp;gt;I: consume OrderPaid
    I-&amp;gt;&amp;gt;I: 扣减库存
    E-&amp;gt;&amp;gt;L: consume OrderPaid
    L-&amp;gt;&amp;gt;L: 创建发货单&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;领域事件的价值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;解耦&lt;/strong&gt;：Order 不需要知道 Inventory 和 Logistics 的存在。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最终一致&lt;/strong&gt;：接受短时间不一致，换取可扩展性。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;审计&lt;/strong&gt;：事件日志本身就是业务轨迹。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;九常见反模式与纠正&#34;&gt;九、常见反模式与纠正&lt;/h2&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;反模式&lt;/th&gt;
					&lt;th&gt;现象&lt;/th&gt;
					&lt;th&gt;纠正&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;贫血领域模型&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Entity 只有字段，Service 全是逻辑&lt;/td&gt;
					&lt;td&gt;把不变规则和状态变更放回实体&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;万能聚合&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;一个聚合包含十几个实体&lt;/td&gt;
					&lt;td&gt;按业务不变量拆分，跨聚合用事件&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;数据库驱动建模&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;先建表，再写代码&lt;/td&gt;
					&lt;td&gt;先建模，再映射持久化&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;跨上下文直接引用&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;Order 直接持有 Inventory 对象&lt;/td&gt;
					&lt;td&gt;只存 ID，通过 ACL/OHS 交互&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;应用层写业务规则&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;if order.status == ...&lt;/code&gt; 散落在 Service&lt;/td&gt;
					&lt;td&gt;下放到领域对象&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;值对象可变&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;money.setAmount(100)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;用 final 类 + 不可变字段，替换而非修改&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十什么时候用-ddd什么时候不用&#34;&gt;十、什么时候用 DDD，什么时候不用？&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    A[项目是否复杂？] --&amp;gt;|否| B[简单 CRUD 即可]
    A --&amp;gt;|是| C[业务是否快速变化？]
    C --&amp;gt;|否| D[事务脚本 / 简单分层]
    C --&amp;gt;|是| E[团队是否愿意投入统一语言？]
    E --&amp;gt;|否| F[DDD 收益低]
    E --&amp;gt;|是| G[DDD 值得尝试]
    G --&amp;gt; H[从核心域开始&amp;lt;br/&amp;gt;小步迭代]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;适合 DDD&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;业务复杂、领域专家与开发需紧密协作&lt;/li&gt;
&lt;li&gt;核心域需要长期演进&lt;/li&gt;
&lt;li&gt;微服务边界模糊，需要战略设计&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;不适合 DDD&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;简单数据录入/展示系统&lt;/li&gt;
&lt;li&gt;生命周期短、需求稳定的项目&lt;/li&gt;
&lt;li&gt;团队没有领域专家时间投入&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十一实战案例新项目从-0-到-1-落地-ddd&#34;&gt;十一、实战案例：新项目从 0 到 1 落地 DDD&lt;/h2&gt;
&lt;p&gt;前面讲的都是概念和 Demo，下面以 &lt;strong&gt;在线健身私教平台 FitCoach&lt;/strong&gt; 为例，把“如何划分领域、子域、限界上下文、微服务，以及如何决策充血/贫血模型、如何裁剪 DDD”串成一条完整思路。&lt;/p&gt;
&lt;h3 id=&#34;111-项目背景与目标&#34;&gt;11.1 项目背景与目标&lt;/h3&gt;
&lt;p&gt;FitCoach 要做一个连接私教与学员的平台，核心能力包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;学员购买课程包、预约上课时间、查看训练记录&lt;/li&gt;
&lt;li&gt;教练管理学员、制定训练计划、记录每次上课内容&lt;/li&gt;
&lt;li&gt;运营人员配置课程 SKU、促销活动、处理退款申诉&lt;/li&gt;
&lt;li&gt;对接第三方支付、短信推送、以及第三方直播上课工具&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;业务不算极端复杂，但涉及“课程、预约、课时、计划、会员、支付、营销”多个概念，且预计会快速迭代。&lt;strong&gt;这正是 DDD 能帮上忙的地方：先把业务地图画清楚，再决定怎么写代码。&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;112-第一步识别领域与子域&#34;&gt;11.2 第一步：识别领域与子域&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;领域（Domain）&lt;/strong&gt; 就是整个健身私教业务要解决的问题空间。通过事件风暴或用户旅程，可以识别出以下子域：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;子域&lt;/th&gt;
					&lt;th&gt;类型&lt;/th&gt;
					&lt;th&gt;判断理由&lt;/th&gt;
					&lt;th&gt;投入策略&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;私教课程与预约&lt;/td&gt;
					&lt;td&gt;核心域&lt;/td&gt;
					&lt;td&gt;直接决定用户体验和变现效率&lt;/td&gt;
					&lt;td&gt;投入最好的领域专家和工程师&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;训练计划与记录&lt;/td&gt;
					&lt;td&gt;核心域&lt;/td&gt;
					&lt;td&gt;教练专业能力的载体，差异化关键&lt;/td&gt;
					&lt;td&gt;与课程域紧密协作演进&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;会员与教练档案&lt;/td&gt;
					&lt;td&gt;支撑域&lt;/td&gt;
					&lt;td&gt;需要定制，但不是竞争壁垒&lt;/td&gt;
					&lt;td&gt;内部实现，够用即可&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;支付与财务&lt;/td&gt;
					&lt;td&gt;通用域&lt;/td&gt;
					&lt;td&gt;行业通用能力&lt;/td&gt;
					&lt;td&gt;对接微信/支付宝，封装标准接口&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;消息通知&lt;/td&gt;
					&lt;td&gt;通用域&lt;/td&gt;
					&lt;td&gt;短信/推送/站内信&lt;/td&gt;
					&lt;td&gt;使用第三方 SaaS&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;内容（文章/视频）&lt;/td&gt;
					&lt;td&gt;支撑域&lt;/td&gt;
					&lt;td&gt;运营辅助，提升留存&lt;/td&gt;
					&lt;td&gt;可用开源 CMS 改造&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;划分子域的权衡要点&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;不要从技术模块出发&lt;/strong&gt;。例如不要先想“需要一个 UserService”，而要想“会员管理”这个业务能力。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心域通常只有 1-2 个&lt;/strong&gt;。如果把所有子域都标成核心，等于没有重点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通用域不要自己造轮子&lt;/strong&gt;。支付、通知直接买，团队精力留给核心域。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;子域边界可以随业务演化&lt;/strong&gt;。MVP 阶段可以把“训练计划”放在“课程域”里，等业务复杂后再拆出来。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;113-第二步划分限界上下文&#34;&gt;11.3 第二步：划分限界上下文&lt;/h3&gt;
&lt;p&gt;划分的核心问题是：&lt;strong&gt;同一个业务名词，在不同地方含义是否不同？生命周期是否独立？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;以“课程”这个词为例：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;上下文&lt;/th&gt;
					&lt;th&gt;“课程”在该上下文中的含义&lt;/th&gt;
					&lt;th&gt;关键概念&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;课程目录上下文&lt;/td&gt;
					&lt;td&gt;可售卖的课程 SKU：价格、有效期、适用人群、退费规则&lt;/td&gt;
					&lt;td&gt;CourseCatalog, PricePolicy&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;预约调度上下文&lt;/td&gt;
					&lt;td&gt;一次具体的上课时间：教练、学员、时段、状态&lt;/td&gt;
					&lt;td&gt;Appointment, TimeSlot&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;训练记录上下文&lt;/td&gt;
					&lt;td&gt;一次实际上课产生的记录：消耗课时、训练内容、教练反馈&lt;/td&gt;
					&lt;td&gt;TrainingRecord, ConsumedSession&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;营销上下文&lt;/td&gt;
					&lt;td&gt;参与促销的课程范围：满减、套餐、优惠券适用&lt;/td&gt;
					&lt;td&gt;PromotionRule, Bundle&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;财务上下文&lt;/td&gt;
					&lt;td&gt;课程订单的应收、实收、退款流水&lt;/td&gt;
					&lt;td&gt;CourseOrder, Refund&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;因为“课程”的含义和生命周期在每个上下文里都不同，所以应该拆成不同的限界上下文。FitCoach 最终划分如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;课程目录上下文&lt;/strong&gt;（Course Catalog Context）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;预约调度上下文&lt;/strong&gt;（Scheduling Context）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;训练记录上下文&lt;/strong&gt;（Training Record Context）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;会员上下文&lt;/strong&gt;（Member Context）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;教练上下文&lt;/strong&gt;（Coach Context）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;支付上下文&lt;/strong&gt;（Payment Context）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;营销上下文&lt;/strong&gt;（Marketing Context）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;消息通知上下文&lt;/strong&gt;（Notification Context）&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;关键判断&lt;/strong&gt;：如果两个地方对同一个名词的定义和生命周期不同，就倾向于拆成不同上下文；如果概念高度一致且总是一起变化，可以合并。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;114-第三步推导微服务边界&#34;&gt;11.4 第三步：推导微服务边界&lt;/h3&gt;
&lt;p&gt;默认规则是&lt;strong&gt;一个限界上下文对应一个微服务&lt;/strong&gt;，但 FitCoach 还要考虑团队规模、数据一致性、发布频率、性能特征。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;限界上下文&lt;/th&gt;
					&lt;th&gt;微服务决策&lt;/th&gt;
					&lt;th&gt;决策理由&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;课程目录&lt;/td&gt;
					&lt;td&gt;独立服务&lt;/td&gt;
					&lt;td&gt;变化频繁，运营经常调整 SKU 和价格&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;预约调度&lt;/td&gt;
					&lt;td&gt;独立服务&lt;/td&gt;
					&lt;td&gt;高并发读，学员大量刷可选时段，需单独伸缩&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;训练记录&lt;/td&gt;
					&lt;td&gt;独立服务&lt;/td&gt;
					&lt;td&gt;写多读少，数据量大，未来可能要独立归档&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;会员 + 教练档案&lt;/td&gt;
					&lt;td&gt;合并为“账户服务”&lt;/td&gt;
					&lt;td&gt;MVP 阶段都很薄，且登录后经常一起查询；内部用模块隔离&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;支付&lt;/td&gt;
					&lt;td&gt;独立服务&lt;/td&gt;
					&lt;td&gt;涉及资金安全、合规审计、对接第三方&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;营销&lt;/td&gt;
					&lt;td&gt;独立服务&lt;/td&gt;
					&lt;td&gt;活动频繁，不能影响核心预约链路&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;消息通知&lt;/td&gt;
					&lt;td&gt;独立服务&lt;/td&gt;
					&lt;td&gt;可被多个业务复用，适合独立演进&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;合并的例外处理&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;会员和教练虽然业务概念不同，但 MVP 阶段数据模型简单，合并可以减少一次远程调用。&lt;/li&gt;
&lt;li&gt;如果后续教练业务复杂化（排期、评级、分成、资质认证），再把教练上下文拆成独立服务。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;一句话&lt;/strong&gt;：微服务边界从限界上下文出发，再结合团队、性能、合规、数据一致性做微调。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;115-第四步选择上下文映射模式&#34;&gt;11.5 第四步：选择上下文映射模式&lt;/h3&gt;
&lt;p&gt;不同上下文之间不是孤立的，需要显式定义它们如何协作。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;graph LR
    A[预约上下文] --&amp;gt;|Customer-Supplier| B[课程目录上下文]
    C[训练记录上下文] --&amp;gt;|Customer-Supplier| A
    D[支付上下文] --&amp;gt;|OHS + PL| E[预约上下文]
    F[营销上下文] --&amp;gt;|Shared Kernel| A
    G[消息通知上下文] --&amp;gt;|Conformist| H[短信/推送第三方]
    I[账户服务] --&amp;gt;|OHS + PL| J[所有业务服务]
    K[训练记录上下文] --&amp;gt;|ACL| L[直播上课工具]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;关系&lt;/th&gt;
					&lt;th&gt;模式&lt;/th&gt;
					&lt;th&gt;说明&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;预约 → 课程目录&lt;/td&gt;
					&lt;td&gt;客户-供应商&lt;/td&gt;
					&lt;td&gt;预约需要课程的名称、价格、时长；课程目录提供稳定查询接口&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;训练记录 → 预约&lt;/td&gt;
					&lt;td&gt;客户-供应商&lt;/td&gt;
					&lt;td&gt;训练记录基于已完成的预约生成&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;支付 → 各业务&lt;/td&gt;
					&lt;td&gt;开放主机服务 OHS&lt;/td&gt;
					&lt;td&gt;支付提供统一的收款、退款、对账 API&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;营销 → 预约&lt;/td&gt;
					&lt;td&gt;共享内核 Shared Kernel&lt;/td&gt;
					&lt;td&gt;促销分摊、优惠券核销规则需要双方共同维护&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;消息通知 → 第三方&lt;/td&gt;
					&lt;td&gt;遵奉者 Conformist&lt;/td&gt;
					&lt;td&gt;直接调用第三方 SDK，模型由第三方定义&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;训练记录 → 直播工具&lt;/td&gt;
					&lt;td&gt;防腐层 ACL&lt;/td&gt;
					&lt;td&gt;直播工具接口不稳定，通过 ACL 隔离，保护领域模型&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;116-第五步充血模型-vs-贫血模型决策&#34;&gt;11.6 第五步：充血模型 vs 贫血模型决策&lt;/h3&gt;
&lt;p&gt;DDD 并不要求全场充血。&lt;strong&gt;模型选择应该看该上下文的业务复杂度&lt;/strong&gt;，而不是追求统一风格。&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;上下文&lt;/th&gt;
					&lt;th&gt;推荐模型&lt;/th&gt;
					&lt;th&gt;理由&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;预约调度&lt;/td&gt;
					&lt;td&gt;充血模型&lt;/td&gt;
					&lt;td&gt;状态机复杂：待预约 → 已预约 → 已签到 → 已完成 → 已取消；有取消规则、爽约规则&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;训练记录&lt;/td&gt;
					&lt;td&gt;充血模型&lt;/td&gt;
					&lt;td&gt;记录不可变，但课时消耗计算、教练评分聚合有业务规则&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;课程目录&lt;/td&gt;
					&lt;td&gt;贫血模型可接受&lt;/td&gt;
					&lt;td&gt;主要是 CRUD，业务规则少&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;账户服务&lt;/td&gt;
					&lt;td&gt;贫血模型&lt;/td&gt;
					&lt;td&gt;数据管理为主，验证规则简单&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;消息通知&lt;/td&gt;
					&lt;td&gt;贫血/过程式&lt;/td&gt;
					&lt;td&gt;基本是发送逻辑，无复杂领域规则&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;营销&lt;/td&gt;
					&lt;td&gt;充血模型&lt;/td&gt;
					&lt;td&gt;优惠计算、互斥规则、分摊逻辑复杂&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;预约上下文的充血模型示例&lt;/strong&gt;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;Appointment&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; AppointmentId id;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; TimeSlot timeSlot;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; MemberId memberId;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; CoachId coachId;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; AppointmentStatus status;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;private&lt;/span&gt; List&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&lt;/span&gt;DomainEvent&lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; domainEvents &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; ArrayList&lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cancel&lt;/span&gt;(LocalDateTime now) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (status &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; AppointmentStatus.&lt;span style=&#34;color:#a6e22e&#34;&gt;BOOKED&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; DomainException(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;只能取消已预约的课程&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (timeSlot.&lt;span style=&#34;color:#a6e22e&#34;&gt;start&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;minusHours&lt;/span&gt;(24).&lt;span style=&#34;color:#a6e22e&#34;&gt;isBefore&lt;/span&gt;(now)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; DomainException(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;开课 24 小时内不可取消&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;status&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; AppointmentStatus.&lt;span style=&#34;color:#a6e22e&#34;&gt;CANCELLED&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        domainEvents.&lt;span style=&#34;color:#a6e22e&#34;&gt;add&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; AppointmentCancelled(id, now));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;checkIn&lt;/span&gt;(LocalDateTime now) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; (status &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; AppointmentStatus.&lt;span style=&#34;color:#a6e22e&#34;&gt;BOOKED&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#66d9ef&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; DomainException(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;未预约不能签到&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#66d9ef&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;status&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; AppointmentStatus.&lt;span style=&#34;color:#a6e22e&#34;&gt;COMPLETED&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        domainEvents.&lt;span style=&#34;color:#a6e22e&#34;&gt;add&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;new&lt;/span&gt; AppointmentCheckedIn(id, memberId, coachId, now));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
  &lt;/div&gt;&lt;p&gt;&lt;strong&gt;决策口诀&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;状态机复杂、不变量多、业务规则变化快 → &lt;strong&gt;充血模型&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;主要是增删改查、数据为主、规则简单 → &lt;strong&gt;贫血模型即可&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;不确定时，先写贫血，等规则复杂了再重构为充血&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;117-第六步ddd-是否可以裁剪&#34;&gt;11.7 第六步：DDD 是否可以裁剪？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;可以，而且应该根据项目情况裁剪。&lt;/strong&gt; DDD 不是全有或全无的框架。&lt;/p&gt;
&lt;h4 id=&#34;场景-1只用战略设计战术设计从简&#34;&gt;场景 1：只用战略设计，战术设计从简&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;适合情况&lt;/strong&gt;：业务复杂但技术栈简单、团队经验不足、MVP 阶段要快速上线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FitCoach 初期做法&lt;/strong&gt;：先画好限界上下文和微服务边界，服务内部用简单 CRUD + Service 实现。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;价值&lt;/strong&gt;：至少解决了“边界混乱、术语不一致、重复造轮子”的问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;场景-2只用战术设计在已有边界内建模&#34;&gt;场景 2：只用战术设计，在已有边界内建模&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;适合情况&lt;/strong&gt;：系统边界已经清楚，但某个模块内部逻辑混乱（如遗留系统重构）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FitCoach 示例&lt;/strong&gt;：课程目录已经稳定，但预约状态机满是 if/else，可以只对“预约上下文”做充血模型重构。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;风险&lt;/strong&gt;：如果战略边界本身不清晰，战术设计会变成“在错误的地方做正确的事”。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;场景-3战略--战术都用但不用高级模式&#34;&gt;场景 3：战略 + 战术都用，但不用高级模式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;这是最常见的落地方式&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;FitCoach 可以用：限界上下文 + 充血模型 + 领域事件 + 分层架构。&lt;/li&gt;
&lt;li&gt;可以先不用：事件溯源、CQRS、六边形架构的严格形式。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;裁剪原则&#34;&gt;裁剪原则&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;复杂的地方用 DDD，简单的地方不要硬套&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先战略后战术&lt;/strong&gt;，边界不清不要深入建模型。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一个项目可以不同上下文用不同成熟度&lt;/strong&gt;：核心域充血 + 贫血，通用域直接用 SDK。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要为了 DDD 而 DDD&lt;/strong&gt;，目标是降低业务复杂度，不是写出“DDD 标准代码”。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;118-落地时间线建议&#34;&gt;11.8 落地时间线建议&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;阶段&lt;/th&gt;
					&lt;th&gt;目标&lt;/th&gt;
					&lt;th&gt;关键产出&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Week 1-2&lt;/td&gt;
					&lt;td&gt;事件风暴 + 统一语言&lt;/td&gt;
					&lt;td&gt;子域图、限界上下文图、术语表&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Week 3&lt;/td&gt;
					&lt;td&gt;微服务边界与映射模式&lt;/td&gt;
					&lt;td&gt;服务拆分方案、接口契约初稿&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Week 4-6&lt;/td&gt;
					&lt;td&gt;核心域战术设计&lt;/td&gt;
					&lt;td&gt;聚合、实体、值对象、领域事件、测试&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Week 7-8&lt;/td&gt;
					&lt;td&gt;基础设施 + 集成测试&lt;/td&gt;
					&lt;td&gt;数据库、消息队列、CI/CD 流水线&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;持续&lt;/td&gt;
					&lt;td&gt;演进&lt;/td&gt;
					&lt;td&gt;根据业务变化调整边界和模型&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;新手常犯的错误&lt;/strong&gt;：一上来就在所有服务里写聚合根、值对象、领域事件。正确做法是先画地图，再对核心域精雕细琢，其他域能跑通即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;十二一句话总结&#34;&gt;十二、一句话总结&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;DDD = 用“统一语言”沟通业务，用“限界上下文”划分边界并推导微服务边界，用“聚合/实体/值对象”封装领域知识，用“领域事件”连接上下文，让软件结构真正反映业务结构。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;把 &lt;a href=&#34;https://gitee.com/soulnew/ddd-demo&#34;&gt;soulnew/ddd-demo&lt;/a&gt; 的 Demo 跑一遍，再画一张你自己业务的战略地图，并尝试标出可能的微服务边界，你就已经比 80% 自称做 DDD 的人更接近它的本质了。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;参考&#34;&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Eric Evans. &lt;em&gt;Domain-Driven Design: Tackling Complexity in the Heart of Software&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Vaughn Vernon. &lt;em&gt;Implementing Domain-Driven Design&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;Vaughn Vernon. &lt;em&gt;Domain-Driven Design Distilled&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
    </item>
    <item>
      <title>《软件架构：最难的部分》详解：分布式架构的权衡分析实战指南</title>
      <link>https://ftmao.com/blog/bookstore/software-architecture-hard-parts-book-notes/</link>
      <pubDate>Wed, 26 Jul 2023 14:00:00 +0800</pubDate>
      <guid>https://ftmao.com/blog/bookstore/software-architecture-hard-parts-book-notes/</guid>
      <description>&lt;h1 id=&#34;软件架构最难的部分详解分布式架构的权衡分析实战指南&#34;&gt;《软件架构：最难的部分》详解：分布式架构的权衡分析实战指南&lt;/h1&gt;
&lt;p&gt;《软件架构：最难的部分》（Software Architecture: The Hard Parts: Modern Trade-Off Analyses for Distributed Architectures）是 O&amp;rsquo;Reilly 在 2021 年出版的一本架构书，四位作者都是业内响当当的人物：Neal Ford（《软件架构 fundamentals》作者之一）、Mark Richards（资深架构师、《软件架构 fundamentals》合著者）、Pramod Sadalage（数据库演进专家）和 Zhamak Dehghani（&amp;ldquo;数据网格&amp;quot;概念的提出者）。&lt;/p&gt;
&lt;p&gt;这本书回答了一个非常现实的问题：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;当我把一个单体系统拆成微服务时，每一步到底该怎么拆？怎么知道拆得对不对？&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;市面上的架构书大多告诉你&amp;quot;什么是好架构&amp;rdquo;，却很少教你&amp;quot;做决策的过程&amp;quot;。这本书补上了这块空白。它的核心立场是：&lt;strong&gt;架构里不存在&amp;quot;最佳实践&amp;quot;，只存在&amp;quot;权衡分析&amp;quot;（Trade-off Analysis）&lt;/strong&gt;。每一个决策都是在拿一些东西换另一些东西，架构师的工作就是把交换的条件摆清楚。&lt;/p&gt;
&lt;p&gt;本文面向&lt;strong&gt;从未接触过软件架构概念&lt;/strong&gt;的读者，按全书十五章的顺序逐层展开，配合图解和实例，帮你建立完整的知识地图。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第-1-章-当没有最佳实践时会发生什么&#34;&gt;第 1 章 当没有&amp;quot;最佳实践&amp;quot;时会发生什么&lt;/h2&gt;
&lt;h3 id=&#34;一切皆权衡&#34;&gt;一切皆权衡&lt;/h3&gt;
&lt;p&gt;作者开篇就泼了一盆冷水：如果有人问你&amp;quot;微服务和单体哪个好&amp;quot;，唯一正确的回答是——&lt;strong&gt;看情况&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;架构决策和买东西很像。假如有人不问预算、不问用途，直接告诉你&amp;quot;买这辆车是最好的&amp;quot;，你肯定会觉得他不靠谱。架构也一样：任何脱离上下文的&amp;quot;最佳实践&amp;quot;都是耍流氓。架构师真正的工作流程是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;找到决策涉及的各个维度（性能、成本、耦合度、团队结构……）；&lt;/li&gt;
&lt;li&gt;分析每个维度上的得与失；&lt;/li&gt;
&lt;li&gt;把分析结果呈现给利益相关者，让他们在知情的情况下做选择。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以这本书的副标题才是&amp;quot;现代分布式架构的权衡分析&amp;quot;——&lt;strong&gt;最难的部分不是技术本身，而是权衡&lt;/strong&gt;。&lt;/p&gt;
&lt;h3 id=&#34;贯穿全书的案例sysops-squad-工单系统&#34;&gt;贯穿全书的案例：Sysops Squad 工单系统&lt;/h3&gt;
&lt;p&gt;为了方便讲解，作者虚构了一个贯穿全书的案例。Sysops Squad 是一家 IT 运维支持公司，客户（通常是企业的 IT 部门）遇到设备故障时提交&lt;strong&gt;工单（Ticket）&lt;/strong&gt;，系统把工单分配给合适的运维专家（Sysop），专家上门或远程修复，完成后填写处理报告。&lt;/p&gt;
&lt;p&gt;他们现在用的是一个大单体应用，随着业务增长越来越痛苦：改一个小功能要全量发布、一个模块的 bug 拖垮整个系统、高峰期扩容只能整体扩容。于是团队决定把它拆成微服务——&lt;strong&gt;这本书讲的就是这一路上遇到的每一个&amp;quot;难啃的骨头&amp;quot;&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 拆分前[&amp;#34;拆分前：单体应用&amp;#34;]
        M[&amp;#34;Sysops Squad 单体系统&amp;lt;br/&amp;gt;工单 + 用户 + 派单 + 报告 + 计费&amp;lt;br/&amp;gt;共享一个大数据库&amp;#34;]
    end
    subgraph 拆分后[&amp;#34;拆分后：微服务&amp;#34;]
        A[&amp;#34;工单服务&amp;#34;]
        B[&amp;#34;用户服务&amp;#34;]
        C[&amp;#34;派单服务&amp;#34;]
        D[&amp;#34;报告服务&amp;#34;]
        E[&amp;#34;计费服务&amp;#34;]
        DB1[(&amp;#34;工单库&amp;#34;)]
        DB2[(&amp;#34;用户库&amp;#34;)]
        DB3[(&amp;#34;派单库&amp;#34;)]
        DB4[(&amp;#34;报告库&amp;#34;)]
        DB5[(&amp;#34;计费库&amp;#34;)]
        A --- DB1
        B --- DB2
        C --- DB3
        D --- DB4
        E --- DB5
    end
    拆分前 --&amp;gt;|本书要讲的全过程| 拆分后&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;看起来很美好对吧？但魔鬼全在细节里：组件怎么划？数据库怎么拆？服务粒度多细合适？公共代码怎么办？跨服务的事务和数据访问怎么办？——这些就是后面十四章要逐一解决的问题。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第-2-章-识别软件架构中的耦合&#34;&gt;第 2 章 识别软件架构中的耦合&lt;/h2&gt;
&lt;h3 id=&#34;架构量子architecture-quantum&#34;&gt;架构量子（Architecture Quantum）&lt;/h3&gt;
&lt;p&gt;这是全书最重要的概念之一，也是后续所有分析的基本单位。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;架构量子&lt;/strong&gt;：系统中可以&lt;strong&gt;独立部署&lt;/strong&gt;的最小单元，包含它正常运行所必需的一切（代码、数据库、配置等）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;判断一个东西是不是一个架构量子，问三个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;能独立部署吗？&lt;/strong&gt;（改完它不用动别的部分就能上线）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;功能内聚性高吗？&lt;/strong&gt;（它内部的代码是围绕同一个业务目标组织的）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;耦合度高吗？&lt;/strong&gt;（量子内部耦合高没关系，量子之间耦合低才是目标）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个单体应用就是一个架构量子；一套拆得干净的微服务，每个服务各自是一个量子；而&amp;quot;服务拆了但数据库还共享&amp;quot;的系统，量子边界其实还卡在数据库上——这是新手最容易掉进去的坑。&lt;/p&gt;
&lt;h3 id=&#34;静态耦合与动态耦合&#34;&gt;静态耦合与动态耦合&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;静态耦合&lt;/strong&gt;：编译期/部署期就能看到的依赖。比如 A 服务调用了共享库里的类，或者两个服务连同一个数据库 schema。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态耦合&lt;/strong&gt;：运行时才发生的调用关系。比如订单服务运行时调用支付服务的接口。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;动态耦合由三个维度决定，这是全书反复使用的分析框架：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;维度&lt;/th&gt;
					&lt;th&gt;两个选项&lt;/th&gt;
					&lt;th&gt;权衡点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;通信（Communication）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;同步 / 异步&lt;/td&gt;
					&lt;td&gt;同步简单直观，异步解耦但复杂&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;一致性（Consistency）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;原子 / 最终&lt;/td&gt;
					&lt;td&gt;原子一致安全，最终一致可用性高&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;协调（Coordination）&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;编排 / 编制&lt;/td&gt;
					&lt;td&gt;编排好管理，编制无中心瓶颈&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这三个维度像调色盘的三原色，后面讲工作流（第 11 章）和 Saga（第 12 章）时会反复用到。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第-3-章-架构模块化&#34;&gt;第 3 章 架构模块化&lt;/h2&gt;
&lt;p&gt;为什么要模块化？作者给出了五个驱动力，每一个都对应一种&amp;quot;不模块化会疼&amp;quot;的场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;可维护性（Maintainability）&lt;/strong&gt;：大泥球代码改一处坏三处，模块化让改动范围可控。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可测试性（Testability）&lt;/strong&gt;：边界清晰的模块可以单独写测试，不用拉起整个系统。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可部署性（Deployability）&lt;/strong&gt;：小模块发布风险低、频率高；单体发布一次像发射火箭。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可扩展性（Scalability）&lt;/strong&gt;：旺季只有&amp;quot;下单&amp;quot;需要扩容，模块化后可以只扩这一个模块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可用性与容错（Availability/Fault Tolerance）&lt;/strong&gt;：故障被限制在模块内部，不至于全站瘫痪。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举个生活化的例子：老式组合音响里一个喇叭坏了，整台机器都得拆开修；而现在的家庭影院，功放、音箱、播放器各自独立，坏哪个换哪个——这就是模块化的价值。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第-4-章-架构分解&#34;&gt;第 4 章 架构分解&lt;/h2&gt;
&lt;p&gt;拆分之前，先要学会&amp;quot;测量&amp;quot;耦合，而不是凭感觉。&lt;/p&gt;
&lt;h3 id=&#34;传入耦合与传出耦合&#34;&gt;传入耦合与传出耦合&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;传入耦合（Afferent Coupling）&lt;/strong&gt;：有多少别的组件依赖我？——衡量&amp;quot;我有多重要、改动我的风险有多大&amp;quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;传出耦合（Efferent Coupling）&lt;/strong&gt;：我依赖多少别的组件？——衡量&amp;quot;我有多容易被别人拖累&amp;quot;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;主序列距离distance-from-the-main-sequence&#34;&gt;主序列距离（Distance from the Main Sequence）&lt;/h3&gt;
&lt;p&gt;这是 Robert C. Martin 提出的一组度量。想象一条坐标轴：横轴是&lt;strong&gt;抽象性&lt;/strong&gt;（组件里抽象类/接口占的比例），纵轴是&lt;strong&gt;不稳定性&lt;/strong&gt;（传出耦合占比）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个组件如果&lt;strong&gt;很具体但很多人依赖它&lt;/strong&gt;（比如工具类被全系统引用），它落在&amp;quot;痛苦区&amp;quot;：难改、一改全崩。&lt;/li&gt;
&lt;li&gt;一个组件如果&lt;strong&gt;很抽象但没人用&lt;/strong&gt;（比如过度设计的框架），它落在&amp;quot;无用区&amp;quot;。&lt;/li&gt;
&lt;li&gt;理想位置在两者之间的对角线（&amp;ldquo;主序列&amp;rdquo;）附近。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些度量不是让你拿计算器天天算，而是提供一个&lt;strong&gt;客观的拆分依据&lt;/strong&gt;：拆分候选的组件，先看它们的耦合度量，优先拆那些传入传出耦合低、边界清晰的——它们是&amp;quot;最容易摘下来的果子&amp;quot;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第-5-章-基于组件的分解模式&#34;&gt;第 5 章 基于组件的分解模式&lt;/h2&gt;
&lt;p&gt;这是全书最&amp;quot;实操&amp;quot;的一章：怎么把一坨单体代码拆成组件？作者给出了一个六步流水线，每一步都有对应的&amp;quot;适应度函数&amp;quot;（自动化检查规则）来保证不跑偏。&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    S1[&amp;#34;① 识别并确定组件大小&amp;lt;br/&amp;gt;盘点代码库里有哪些组件、各占多大比例&amp;#34;]
    S2[&amp;#34;② 收集公共组件&amp;lt;br/&amp;gt;把多个组件里重复出现的代码收拢到一处&amp;#34;]
    S3[&amp;#34;③ 扁平化组件&amp;lt;br/&amp;gt;把代码从根命名空间下沉到有明确职责的叶子节点&amp;#34;]
    S4[&amp;#34;④ 确定组件依赖&amp;lt;br/&amp;gt;画出组件依赖图，找出纠缠在一起的循环依赖&amp;#34;]
    S5[&amp;#34;⑤ 创建组件域&amp;lt;br/&amp;gt;把职责相近的组件归入同一个领域分组&amp;#34;]
    S6[&amp;#34;⑥ 创建领域服务&amp;lt;br/&amp;gt;每个领域组对外暴露成一个粗粒度服务&amp;#34;]
    S1 --&amp;gt; S2 --&amp;gt; S3 --&amp;gt; S4 --&amp;gt; S5 --&amp;gt; S6&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;几个步骤的直觉解释：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第①步&lt;/strong&gt;像搬家前盘点家当：先知道自己有什么。适应度函数可以自动检查&amp;quot;任何组件不得超过代码库总量的 X%&amp;quot;、&amp;ldquo;组件大小不得偏离平均值 N 个标准差&amp;rdquo;，防止拆出巨型组件。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第②步&lt;/strong&gt;解决重复：如果&amp;quot;日志工具&amp;quot;&amp;ldquo;日期处理&amp;quot;在十个组件里各抄了一份，先把它们收拢，否则拆开后会十处同步改。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第③步&lt;/strong&gt;针对一种常见病：大量代码堆在根命名空间（比如所有代码都在 &lt;code&gt;com.sysops&lt;/code&gt; 下面不分层），说明系统从来没有真正的结构，必须先给每段代码找到归属。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第④步&lt;/strong&gt;是分水岭：依赖图里纠缠不清的组件（A 调 B、B 调 C、C 又调 A）说明领域边界划分有误，要回到业务层面重新划。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;第⑤⑥步&lt;/strong&gt;完成从&amp;quot;代码组件&amp;quot;到&amp;quot;业务服务&amp;quot;的跃迁：同一域内的组件将来部署成一个服务，域与域之间的边界就是服务边界。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;**适应度函数（Fitness Function）**是本书反复强调的治理手段：把架构约束写成自动化测试，放进 CI 流水线。比如&amp;quot;报告域的组件不允许依赖计费域的组件&amp;quot;可以直接写成一条 ArchUnit 测试，谁违反了构建就失败——架构约束从&amp;quot;文档里的愿望&amp;quot;变成&amp;quot;会报警的护栏&amp;rdquo;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第-6-章-拆分运营数据&#34;&gt;第 6 章 拆分运营数据&lt;/h2&gt;
&lt;p&gt;代码拆完，真正的硬骨头来了：&lt;strong&gt;数据库&lt;/strong&gt;。很多团队服务拆得很漂亮，底下却还用同一个数据库——等于楼分了户、水电还是一户一表，根本没有真正解耦。&lt;/p&gt;
&lt;h3 id=&#34;拆开数据库的推力与阻力&#34;&gt;拆开数据库的推力与阻力&lt;/h3&gt;
&lt;p&gt;作者在拆数据这件事上建立了一个&amp;quot;天平&amp;quot;模型：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;拆分驱动因素（Data Disintegrators，推着你拆）：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;变更控制：一个服务改表结构，不用和别的团队协调。&lt;/li&gt;
&lt;li&gt;连接管理：数据库连接数有限，服务多了必须分库。&lt;/li&gt;
&lt;li&gt;可扩展性：可以按服务单独扩容数据库。&lt;/li&gt;
&lt;li&gt;容错：一个库挂了只影响一个服务。&lt;/li&gt;
&lt;li&gt;架构量子：共享数据库会把量子边界撑破。&lt;/li&gt;
&lt;li&gt;数据库类型优化：不同服务适合不同数据库（工单用关系库，日志用时序库）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;整合因素（Data Integrators，拽着你不让拆）：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据关系：外键、JOIN 在跨库之后全部失效。&lt;/li&gt;
&lt;li&gt;数据库事务：跨库没有 ACID 事务，必须面对分布式事务的复杂性。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;五步数据库分解法&#34;&gt;五步数据库分解法&lt;/h3&gt;
&lt;p&gt;书里给出了一个务实的渐进式拆分流程，核心思想是&amp;quot;&lt;strong&gt;先逻辑拆，再物理拆&lt;/strong&gt;&amp;quot;：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    P1[&amp;#34;① 分析数据库&amp;lt;br/&amp;gt;划分数据域&amp;#34;]
    P2[&amp;#34;② 把表分配到&amp;lt;br/&amp;gt;各数据域&amp;#34;]
    P3[&amp;#34;③ 数据库连接&amp;lt;br/&amp;gt;按域分离&amp;#34;]
    P4[&amp;#34;④ Schema 迁移到&amp;lt;br/&amp;gt;独立数据库服务器&amp;#34;]
    P5[&amp;#34;⑤ 切换到完全独立的&amp;lt;br/&amp;gt;数据库&amp;#34;]
    P1 --&amp;gt; P2 --&amp;gt; P3 --&amp;gt; P4 --&amp;gt; P5&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;关键是第③步：表还没物理分开，就先让各服务只连自己的 schema、只访问自己的表。这一步不动数据，只动代码和连接，风险小、可回滚。等代码层面确认拆分正确了，再做物理迁移（第④⑤步）。这种&amp;quot;小步快跑、每步可退&amp;quot;的思路贯穿全书。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第-7-章-服务粒度&#34;&gt;第 7 章 服务粒度&lt;/h2&gt;
&lt;p&gt;拆服务时最纠结的问题：&lt;strong&gt;一个服务到底该多大？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;作者又祭出了&amp;quot;天平&amp;quot;分析法——这次叫粒度解耦因素（Granularity Disintegrators，推着你往细拆）与粒度整合因素（Granularity Integrators，拽着你往粗合）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;往细拆的理由（Disintegrators）：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;服务范围与功能&lt;/strong&gt;：一个服务只干一件事。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代码波动性&lt;/strong&gt;：如果系统里&amp;quot;促销规则&amp;quot;每周改、&amp;ldquo;订单核心&amp;quot;半年不动，就该把促销拆出去，免得高频改动反复惊扰稳定部分。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可扩展性与吞吐量&lt;/strong&gt;：只有搜索服务需要扛流量，拆细了才能单独扩容。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;容错&lt;/strong&gt;：非关键功能（比如推荐）挂了不该拖垮下单。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;安全&lt;/strong&gt;：处理支付的服务需要更高的安全等级和审计，独立出来好管控。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可扩展性（Extensibility）&lt;/strong&gt;：新功能以新服务的形式生长，不改老代码。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;往粗合的理由（Integrators）：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据库事务&lt;/strong&gt;：两个操作必须在同一个 ACID 事务里？那它们最好待在同一个服务里。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;工作流与编制&lt;/strong&gt;：跨服务的编排/编制又慢又难调试，粒度太细会制造大量分布式工作流。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;共享代码&lt;/strong&gt;：两个服务共享大量代码，可能说明它们本来就该是一个。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据关系&lt;/strong&gt;：两个服务频繁访问彼此的数据，拆分带来的网络开销和一致性成本可能超过收益。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;实例&lt;/strong&gt;：Sysops Squad 拆出&amp;quot;工单服务&amp;quot;后，争论要不要把&amp;quot;工单指派&amp;quot;再拆成独立服务。分析后发现：指派逻辑改动频繁（每周调规则）、需要单独的弹性伸缩（早高峰派单量是平时的 5 倍）——两个 disintegrator 都很强；而它与工单共享的表只有一张、没有跨库事务需求——integrator 很弱。于是拆。反过来，&amp;ldquo;工单备注&amp;quot;功能没有任何独立的扩展和发布需求，就留在工单服务里。&lt;/p&gt;
&lt;p&gt;这就是权衡分析的样子：&lt;strong&gt;不是数哪边的理由多，而是掂量哪边的理由重&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第-8-章-复用模式&#34;&gt;第 8 章 复用模式&lt;/h2&gt;
&lt;p&gt;多个服务都需要同一段代码（比如审计日志、认证），怎么办？书里给了四种方案：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;模式&lt;/th&gt;
					&lt;th&gt;做法&lt;/th&gt;
					&lt;th&gt;优点&lt;/th&gt;
					&lt;th&gt;缺点&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;代码复制&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;每个服务拷一份&lt;/td&gt;
					&lt;td&gt;零耦合、零依赖&lt;/td&gt;
					&lt;td&gt;改 bug 要改 N 处&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;共享库&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;打成 jar/npm 包共用&lt;/td&gt;
					&lt;td&gt;版本可控、无运行时开销&lt;/td&gt;
					&lt;td&gt;版本地狱、异构技术栈难共享&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;共享服务&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;独立部署一个公共服务&lt;/td&gt;
					&lt;td&gt;改动一处全生效、跨语言&lt;/td&gt;
					&lt;td&gt;网络开销、单点风险、变更影响面大&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;Sidecar / 服务网格&lt;/strong&gt;&lt;/td&gt;
					&lt;td&gt;功能放进每个服务的伴生容器&lt;/td&gt;
					&lt;td&gt;跨语言、运维统一&lt;/td&gt;
					&lt;td&gt;需要容器编排基础设施&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;选择思路很直接：&lt;strong&gt;变化越频繁、越偏向业务的代码，越不要共享&lt;/strong&gt;（宁肯复制）；&lt;strong&gt;越稳定、越偏向技术基础设施的能力&lt;/strong&gt;（日志、监控、鉴权），越适合共享或 Sidecar。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实例&lt;/strong&gt;：Sysops Squad 的&amp;quot;专家评分算法&amp;quot;每个服务都在用，但它每月都在调优——选了代码复制，各服务按需演进。而&amp;quot;日志采集&amp;quot;用了 Sidecar，统一由运维团队维护。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第-9-章-数据所有权与分布式事务&#34;&gt;第 9 章 数据所有权与分布式事务&lt;/h2&gt;
&lt;p&gt;拆库之后立刻撞上两个难题：一张表被两个服务都要写，归谁？一个业务操作要跨两个服务写数据，怎么保证一致性？&lt;/p&gt;
&lt;h3 id=&#34;数据所有权&#34;&gt;数据所有权&lt;/h3&gt;
&lt;p&gt;先看所有权的两种场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;单一所有权&lt;/strong&gt;：一张表只有一个服务写，其他服务最多只读——理想情况。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;共同所有权&lt;/strong&gt;：多个服务都要写同一张表——必须拆开归属。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;处理共同所有权有三种技术：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph 表拆分技术[&amp;#34;表拆分：按列拆开&amp;#34;]
        T1[&amp;#34;customer 表&amp;lt;br/&amp;gt;姓名/联系方式 → 档案服务&amp;lt;br/&amp;gt;信用额度/账单地址 → 计费服务&amp;#34;]
    end
    subgraph 数据域技术[&amp;#34;数据域：按行拆开&amp;#34;]
        T2[&amp;#34;ticket 表&amp;lt;br/&amp;gt;进行中的工单 → 工单服务&amp;lt;br/&amp;gt;已归档的历史工单 → 报告服务&amp;#34;]
    end
    subgraph 委派技术[&amp;#34;委派：一主一从&amp;#34;]
        T3[&amp;#34;invoice 表归计费服务所有&amp;lt;br/&amp;gt;工单服务需要时&amp;lt;br/&amp;gt;通过接口委托计费服务写&amp;#34;]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;h3 id=&#34;分布式事务的三种模式&#34;&gt;分布式事务的三种模式&lt;/h3&gt;
&lt;p&gt;跨服务更新数据时，怎么保持一致？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;后台同步模式&lt;/strong&gt;：先各自写，后台进程定时对账、修复不一致。最简单，但不一致窗口最长，适合对实时性无所谓的数据（比如统计报表）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编排请求模式&lt;/strong&gt;：由一个编排者按顺序调用各服务，全部确认后才算完成。一致性好，但慢，且编排者成了耦合中心。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;基于事件的模式&lt;/strong&gt;：写本地库后立刻发事件，其他服务异步消费。性能和可用性最好，但要想清楚&amp;quot;事件丢了怎么办&amp;rdquo;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;实例&lt;/strong&gt;：工单完成后要同时更新&amp;quot;工单状态&amp;quot;和&amp;quot;专家积分&amp;rdquo;。如果用编排请求，用户要等两个服务都响应；用事件模式，工单服务先发 &lt;code&gt;TicketCompleted&lt;/code&gt; 事件立即返回，积分服务异步加分——用户快了几百毫秒，代价是积分查询页面可能晚几秒才更新。Sysops Squad 选了后者：&lt;strong&gt;最终一致性换了响应速度&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第-10-章-分布式数据访问&#34;&gt;第 10 章 分布式数据访问&lt;/h2&gt;
&lt;p&gt;读操作比写操作更常见：服务 A 需要服务 B 拥有的数据，怎么拿？四种模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;服务间通信&lt;/strong&gt;：直接调 B 的接口。最简单，但每次查询都是一次网络往返，B 挂了 A 也查不了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;列模式复制&lt;/strong&gt;：把需要的那几列数据复制一份到 A 的数据库（通过消息保持同步）。查询快、无运行时依赖，但数据有延迟、要处理重复治理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复制缓存&lt;/strong&gt;：B 把数据推进共享缓存（如 Redis），A 从缓存读。适合读多写少的参考数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据域&lt;/strong&gt;：把双方都要的数据干脆划给一个专门的&amp;quot;数据域服务&amp;quot;管理。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;选择的关键问题是：&amp;quot;&lt;strong&gt;这份数据 A 需要多新鲜？B 的可用性 A 能承受吗？&lt;/strong&gt;&amp;ldquo;答案不同，模式就不同。比如专家的联系信息（几乎不变）适合复制缓存；工单的实时状态（必须最新）只能走服务间通信。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第-11-章-管理分布式工作流&#34;&gt;第 11 章 管理分布式工作流&lt;/h2&gt;
&lt;p&gt;一个业务流程跨多个服务时，谁来当&amp;quot;导演&amp;rdquo;？两种风格：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart TD
    subgraph 编排[&amp;#34;编排 Orchestration：有个总指挥&amp;#34;]
        O[&amp;#34;编排者&amp;lt;br/&amp;gt;（工单流程服务）&amp;#34;]
        O --&amp;gt; A[&amp;#34;工单服务&amp;#34;]
        O --&amp;gt; B[&amp;#34;派单服务&amp;#34;]
        O --&amp;gt; C[&amp;#34;计费服务&amp;#34;]
        O --&amp;gt; D[&amp;#34;通知服务&amp;#34;]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    subgraph 编制[&amp;#34;编制 Choreography：舞者跟着音乐各自起舞&amp;#34;]
        A2[&amp;#34;工单服务&amp;#34;] --&amp;gt;|发事件| B2[&amp;#34;派单服务&amp;#34;]
        B2 --&amp;gt;|发事件| C2[&amp;#34;计费服务&amp;#34;]
        C2 --&amp;gt;|发事件| D2[&amp;#34;通知服务&amp;#34;]
    end&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;编排（Orchestration）&lt;/strong&gt;：设一个中心编排者，像乐队指挥，告诉每个服务&amp;quot;现在该你了&amp;quot;。流程一目了然、好监控、好报错；但编排者容易变成新的&amp;quot;小单体&amp;quot;，且是所有服务的耦合点。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编制（Choreography）&lt;/strong&gt;：没有中心，每个服务监听事件、做完自己的事再发下一个事件，像舞者跟着音乐即兴配合。极度解耦、无单点；但流程分散在各处，&amp;ldquo;现在进行到哪一步了&amp;quot;这种简单问题都很难回答。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还有一个关键细节：**工作流状态放哪？**编排模式下状态天然在编排者手里；编制模式下要么设一个专门的&amp;quot;状态所有者&amp;quot;服务，要么忍受状态分散的痛苦。&lt;/p&gt;
&lt;p&gt;经验法则：&lt;strong&gt;流程复杂、分支多、需要人工介入补偿的，用编排；流程线性、追求吞吐量的，用编制&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第-12-章-事务性-saga&#34;&gt;第 12 章 事务性 Saga&lt;/h2&gt;
&lt;p&gt;Saga 是分布式世界里替代 ACID 事务的方案：把一个跨服务的大事务拆成一串本地小事务，任何一步失败就按相反顺序执行&lt;strong&gt;补偿操作&lt;/strong&gt;（退款、释放库存……）回滚已完成的步骤。&lt;/p&gt;
&lt;p&gt;书里把第 2 章的三个维度组合起来，给 Saga 模式起了八个别致的名字（括号里的三个字母依次代表：一致性 a=原子/e=最终、通信 s=同步/a=异步、协调 o=编排/c=编制）：&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;模式&lt;/th&gt;
					&lt;th&gt;组合&lt;/th&gt;
					&lt;th&gt;适用场景&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;史诗 Saga&lt;/strong&gt;（a,s,o）&lt;/td&gt;
					&lt;td&gt;原子+同步+编排&lt;/td&gt;
					&lt;td&gt;最接近传统事务，但分布式环境下最难实现&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;电话接龙 Saga&lt;/strong&gt;（a,s,c）&lt;/td&gt;
					&lt;td&gt;原子+同步+编制&lt;/td&gt;
					&lt;td&gt;既要原子性又不想有中心——又慢又脆，慎用&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;童话 Saga&lt;/strong&gt;（e,s,o）&lt;/td&gt;
					&lt;td&gt;最终+同步+编排&lt;/td&gt;
					&lt;td&gt;简单好用的常见选择&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;时间旅行 Saga&lt;/strong&gt;（e,s,c）&lt;/td&gt;
					&lt;td&gt;最终+同步+编制&lt;/td&gt;
					&lt;td&gt;高并发下的妥协方案&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;奇幻小说 Saga&lt;/strong&gt;（a,a,o）&lt;/td&gt;
					&lt;td&gt;原子+异步+编排&lt;/td&gt;
					&lt;td&gt;用异步提高吞吐，靠编排守住一致性&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;恐怖故事 Saga&lt;/strong&gt;（a,a,c）&lt;/td&gt;
					&lt;td&gt;原子+异步+编制&lt;/td&gt;
					&lt;td&gt;作者直言：别这么干&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;平行 Saga&lt;/strong&gt;（e,a,o）&lt;/td&gt;
					&lt;td&gt;最终+异步+编排&lt;/td&gt;
					&lt;td&gt;最实用的组合之一&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;strong&gt;文集 Saga&lt;/strong&gt;（e,a,c）&lt;/td&gt;
					&lt;td&gt;最终+异步+编制&lt;/td&gt;
					&lt;td&gt;最解耦、最高可用，也最难调试&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;起这些&amp;quot;俗气&amp;quot;的名字是故意的：&lt;strong&gt;好记&lt;/strong&gt;。团队讨论时说&amp;quot;这个流程用文集模式&amp;rdquo;，比说&amp;quot;最终一致+异步通信+编制协调的 Saga&amp;quot;高效得多。&lt;/p&gt;
&lt;p&gt;核心结论：&lt;strong&gt;越往左（原子一致），越安全越慢越脆；越往右（最终一致+异步+编制），越快越抗造，但你要为&amp;quot;中间状态&amp;quot;和&amp;quot;补偿逻辑&amp;quot;付出工程成本&lt;/strong&gt;。没有免费的午餐，只有明摆着的账单。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第-13-章-契约&#34;&gt;第 13 章 契约&lt;/h2&gt;
&lt;p&gt;服务之间的接口约定（Contract）该多严格？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;严格契约&lt;/strong&gt;：字段类型、格式全部写死并强校验（如带 schema 的 RPC）。好处是错误在编译/调用瞬间就暴露；代价是任何字段变更都要双方同时升级，耦合很紧。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;宽松契约&lt;/strong&gt;：只约定必需字段，其余自由扩展（如 JSON + 约定）。灵活、可独立演进；代价是错误可能潜伏到运行时才爆发。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;还有两个进阶概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;消费者驱动的契约（Consumer-Driven Contracts）&lt;/strong&gt;：由服务的&lt;strong&gt;调用方&lt;/strong&gt;写下&amp;quot;我需要这些字段、这种格式&amp;quot;，作为测试放进提供方的流水线。提供方一改接口，立刻知道会弄坏谁——契约从&amp;quot;提供方说了算&amp;quot;变成&amp;quot;对消费者负责&amp;quot;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;戳记耦合（Stamp Coupling）&lt;/strong&gt;：调用时把一大坨数据结构整个传过去（比如把 50 个字段的 &lt;code&gt;Customer&lt;/code&gt; 对象传给只需要 &lt;code&gt;email&lt;/code&gt; 的服务）。对方被迫依赖整个结构的形状，改任何一个字段都可能波及无辜。&lt;strong&gt;只传对方真正需要的字段&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第-14-章-管理分析型数据&#34;&gt;第 14 章 管理分析型数据&lt;/h2&gt;
&lt;p&gt;运营数据（支撑业务运转）拆给了微服务，那&lt;strong&gt;分析数据&lt;/strong&gt;（做报表、跑 BI、训练模型）怎么办？作者梳理了三代方案：&lt;/p&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;flowchart LR
    DW[&amp;#34;数据仓库&amp;lt;br/&amp;gt;ETL 抽取 → 清洗 → 集中存储&amp;lt;br/&amp;gt;结构化、为报表优化&amp;#34;] --&amp;gt; DL[&amp;#34;数据湖&amp;lt;br/&amp;gt;原始数据先倒进去&amp;lt;br/&amp;gt;用的时候再处理&amp;lt;br/&amp;gt;灵活但容易变成数据沼泽&amp;#34;] --&amp;gt; DM[&amp;#34;数据网格&amp;lt;br/&amp;gt;按领域分布式的数据产品&amp;lt;br/&amp;gt;数据像微服务一样被治理&amp;#34;]&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;**数据网格（Data Mesh）**是本章的高潮，也是作者之一 Zhamak Dehghani 原创的概念。它的核心思想是把微服务的哲学搬到数据世界：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;领域拥有数据&lt;/strong&gt;：工单团队对自己的工单数据负责，而不是甩给中央数据团队。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据即产品&lt;/strong&gt;：数据要有明确的接口、质量承诺、文档，像 API 一样被消费。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;自助式数据平台&lt;/strong&gt;：基础设施团队提供平台，领域团队自助发布数据产品。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;联邦式治理&lt;/strong&gt;：全局规则（隐私、安全）统一，领域内部自治。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;作者也诚实地给出了&amp;quot;什么时候&lt;strong&gt;不要&lt;/strong&gt;用数据网格&amp;quot;：团队小、数据量不大、分析需求简单时，一个数据仓库就够了——&lt;strong&gt;架构量子越少越好，够用就行&lt;/strong&gt;。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;第-15-章-构建你自己的权衡分析&#34;&gt;第 15 章 构建你自己的权衡分析&lt;/h2&gt;
&lt;p&gt;最后一章是方法论总结：这套&amp;quot;天平分析&amp;quot;手艺怎么用到自己的项目里？&lt;/p&gt;
&lt;p&gt;几条核心建议：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;找出纠缠的维度&lt;/strong&gt;：性能和安全往往互相拉扯，先把一个决策涉及的所有维度列出来。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用 MECE 原则列清单&lt;/strong&gt;：相互独立、完全穷尽，避免漏掉某个维度的代价。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;警惕&amp;quot;脱离上下文&amp;quot;的陷阱&lt;/strong&gt;：网上说&amp;quot;Netflix 用了 X 架构所以 X 好&amp;quot;——Netflix 的上下文（规模、团队、预算）和你完全不同，结论不能直接搬。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定性分析优先于定量&lt;/strong&gt;：很多时候没法拿到精确数字，用&amp;quot;高/中/低&amp;quot;的比较级就够做决策了，别为了伪精确而瘫痪。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;只给结论，别淹没听众&lt;/strong&gt;：给业务方汇报时，先说&amp;quot;推荐方案 B，因为……&amp;quot;，细节放附录。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;远离蛇油和布道&lt;/strong&gt;：任何声称&amp;quot;无条件最优&amp;quot;的方案（银弹、框架布道者），默认先怀疑。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;全书总结一张权衡分析的思维导图&#34;&gt;全书总结：一张权衡分析的思维导图&lt;/h2&gt;
&lt;div class=&#34;code-block&#34;&gt;
    &lt;button class=&#34;code-copy&#34; type=&#34;button&#34; hidden aria-label=&#34;Copy code to clipboard&#34;&gt;
      &lt;span class=&#34;code-copy-label&#34; aria-hidden=&#34;true&#34;&gt;Copy&lt;/span&gt;
    &lt;/button&gt;
    &lt;pre tabindex=&#34;0&#34;&gt;&lt;code class=&#34;language-mermaid&#34; data-lang=&#34;mermaid&#34;&gt;mindmap
  root((软件架构&amp;lt;br/&amp;gt;最难的部分))
    核心理念
      没有最佳实践
      一切皆权衡
      架构量子是分析单位
    怎么拆
      组件分解六步法
      数据库五步分解
      粒度天平
        Disintegrators 往细拆
        Integrators 往粗合
    拆完怎么办
      复用四模式
      数据所有权
      分布式数据访问
    跨服务协作
      编排 vs 编制
      八种 Saga
      严格 vs 宽松契约
    数据与分析
      数据仓库/湖
      数据网格
    方法论
      适应度函数
      MECE 分析
      警惕脱离上下文&lt;/code&gt;&lt;/pre&gt;
  &lt;/div&gt;&lt;p&gt;这本书最有价值的不是任何一条具体结论，而是它示范的&lt;strong&gt;思考方式&lt;/strong&gt;：面对任何架构决策，先问&amp;quot;这个选择让我得到什么、失去什么&amp;quot;，把天平两端的砝码摆出来，再结合自己的上下文做判断。&lt;/p&gt;
&lt;p&gt;如果你是架构新手，建议阅读顺序是：先读第 1、2 章建立&amp;quot;权衡&amp;quot;和&amp;quot;架构量子&amp;quot;的心智模型，再读第 5、6、7 章学会&amp;quot;拆&amp;quot;，最后按需查阅第 8–14 章解决&amp;quot;拆完之后&amp;quot;的具体问题。第 15 章适合放在手边，每次做重大技术选型前翻一遍——它会让你少交很多学费。&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>
