1. 业务需求,先把“为什么拆”想清楚
这两年云原生微服务几乎成了后端开发的默认选项,但把一套单体系统真正拆成生产级微服务,前期最难的反而是一些看似平常的问题:业务需求怎么收敛、服务边界怎么切、每项技术栈凭什么这么选。最近我完整复盘了一个零售业务中台项目,从梳理痛点、划分领域边界,到确定注册中心、网关、消息队列、监控体系,整个过程踩了不少坑。这篇内容不谈空泛概念,只记录一个可以复用的决策过程,适合正在准备微服务改造,或者刚拿到一个从零搭建云原生架构任务的团队参考。
很多团队一上来就急着画服务拓扑、选中间件,却忽略了最先应该做的事:搞清楚现在的系统到底为什么撑不住了。这个问题回答不清楚,拆出来的微服务大概率只是把单体复制了 N 份,运维成本直线升高,业务并发却没什么改善。所以这一章先从业务侧讲起。
1.1 原来的单体系统到底卡在哪
我复盘的项目背景很典型:一个零售品牌的线上商城,最初是一套 Java 单体应用,订单、库存、营销、用户、商品全部打在一个 war 包里,多个团队共用同一个代码仓库和同一个数据库。平时日活几千还能撑住,但一到促销节点,整个 Tomcat 和 MySQL 的 CPU 直接飘红,数据库连接被打满,紧接着是一连串雪崩:用户请求超时、下单失败、库存数据对不上。
把问题拆开看,痛点其实集中在这几类:
- 代码耦合严重:一次下单要查用户、查商品、查优惠券、扣库存、生成订单、发消息,所有业务逻辑都写在一个大事务里。表面上保证了强一致,实际成了“慢查询放大器”,一个环节慢,整条链路就超时。
- 资源无法独立扩容:订单服务流量高,但所有模块在同一个进程里,只能整体扩容。内存和 CPU 都被非关键模块浪费,成本高,效果差。
- 发布和协作冲突:多个团队同时改一个仓库,合并代码经常冲突;任何一个模块出现紧急 bug,都会阻塞其他团队的发布窗口。
- 数据模型过度集中:订单表、库存表、营销表混在一个库,表数量庞大,索引和锁竞争越来越严重,DBA 维护困难。
- 故障隔离能力为零:一个接口 OOM,整个应用退出,所有业务全部不可用。
这些问题的根本原因不是技术债,而是“单体的边界”和“业务增长的复杂度”不匹配。所以第一步不是选框架,而是把业务域彻底梳理清楚。
1.2 业务域拆解与限界上下文
做微服务拆分时,我建议先用领域驱动设计里“限界上下文”的思路,而不是按功能模块拍脑袋。具体做法是从业务流程出发,找出所有业务事件,比如用户下单、支付成功、库存锁定、商品上下架、物流履约、通知触达。每个事件都属于某一个明确的业务域,这就是服务划分的候选边界。
我们最终拆出来的核心服务如下:
| 服务名 | 核心职责 | 核心数据 |
|---|---|---|
| 用户服务 | 用户身份、账户、收货地址、会员等级 | 用户表、地址表、积分表 |
| 商品服务 | 商品基本信息、类目、上下架、价格管理 | 商品表、类目表、SKU表 |
| 库存服务 | 库存查询、锁库存、扣减库存、库存回滚 | 库存表、库存流水表 |
| 订单服务 | 订单创建、订单状态流转、订单查询 | 订单表、订单明细表 |
| 支付服务 | 支付单创建、支付回调、退款、对账 | 支付表、退款表 |
| 营销服务 | 优惠券、活动、促销规则、用户权益 | 活动表、优惠券表 |
| 履约服务 | 发货单、物流轨迹、签收确认 | 发货表、物流轨迹表 |
| 通知服务 | 短信、邮件、站内信、推送 | 通知记录表、模板表 |
拆分原则很简单:每个领域的数据由同一个服务独占,其他服务想拿数据只能通过接口或事件,不允许直接跨库查询。刚开始拆的时候不用追求“一个服务一张表”的粒度,更合理的是把强相关数据放进同一个服务,比如订单服务和订单明细服务合并成订单服务,避免频繁的分布式事务。
1.3 非功能性需求和技术约束
业务需求不只是“能下单”,还要回答一组关键指标,不然到后面没法验收。这个项目硬性要求是这样的:整体可用性 99.9%,大促峰值 TPS 3000,订单创建接口 P99 响应时间小于 500 毫秒,支付回调数据不能丢失,支持按环境独立部署和扩容。这些指标直接决定了架构和选型方向。
技术约束同样重要。团队以 Java 为主,运维人力有限,不可能自研中间件,也无法养一个专门的运维团队去维护复杂的服务网格。因此我们倾向于选择云原生领域成熟、社区活跃的开源组件,而不是什么都自己搭。把“生产级”三个字翻译成落地要求,就是可观测、可容灾、可弹性伸缩、可灰度发布,缺一样都不能叫生产级。
2. 架构设计:把一个单体切成一张网
需求清楚之后,架构设计就有了边界。微服务架构不是把模块改成独立进程那么简单,而是要重新设计接入方式、服务通信、数据一致性、故障隔离和部署形态。下面是我这套架构的核心思路。
2.1 整体分层与部署形态
整个系统从上到下可以分成四层,每层只做自己该做的事:
- 接入层:外部流量入口,使用域名、CDN、负载均衡和应用防火墙,最外层挡住恶意请求,并终结 TLS 证书。
- 网关层:Kubernetes Ingress 负责南北向流量转发,Spring Cloud Gateway 作为业务网关,负责鉴权、限流、路由和部分协议转换。
- 服务层:所有业务微服务,按领域边界划分为用户、商品、库存、订单、支付、营销、履约、通知等服务。服务间通过 OpenFeign 同步调用或 RocketMQ 异步解耦。
- 基础能力层:注册中心、配置中心、消息队列、分布式缓存、对象存储、监控告警、日志收集、链路追踪,这些组件构成了微服务运行的底座。
- 数据层:每个服务独享自己的数据库实例或数据库,通过权限隔离,禁止跨服务直接访问。
部署形态上,我们使用 Kubernetes 集群,按环境划分命名空间,比如 dev、staging、prod 完全隔离。每个服务都通过 Helm Chart 发布,通过一套流水线构建镜像并自动部署。网关层内部再区分“外部 API 网关”和“内部服务调用”,避免业务服务被外部直接暴露。
2.2 服务间通信与接口设计
服务拆开后,通信方式必须提前定好,否则会出现“用 Feign 调异步接口”、“用 MQ 做同步请求”这类混乱。我们定的原则很直接:请求响应的实时查询、事务内必要的写操作,用 OpenFeign 同步调用;跨服务最终一致、需要削峰填谷、需要广播通知的操作,用 RocketMQ 异步事件。
接口设计上,统一使用 RESTful 风格,路径以服务名开头,比如/api/order,接口必须带版本号,比如/api/v1/order。字段命名统一用下划线,时间统一传时间戳或用 ISO 8601 字符串,错误码统一返回code/message/trace_id结构。最关键的一点是幂等设计,所有写接口都要求客户端传X-Request-ID,服务端用 Redis 记录请求唯一键,重复提交直接返回第一次的结果。没有这一步,微服务下的网络重试会造成大量重复订单。
2.3 数据一致性与事务策略
微服务最容易被问的问题就是分布式事务。我在实际项目里的结论是:不要追求所有业务都强一致,否则系统会变得极慢极复杂。需要强一致的场景其实很少,更多场景可以接受秒级甚至分钟级最终一致。
针对不同场景,我们用了三种策略:
- 本地事务加事务消息:订单服务和库存服务之间,先开启本地事务创建订单和扣减预占库存流水,同时向 RocketMQ 发送事务消息,等消息确认后再异步更新库存可用数量。这样保证“订单已创建,库存已预占,消息一定发出”这三个动作要么都成功,要么可以补偿。
- 可靠消息最终一致:支付回调场景,支付服务接收回调后修改支付单状态,再发送“支付成功”事件;订单服务消费事件后更新订单状态,并触发积分、通知等下游动作。只要消息不丢,下游不断重试,最终就是一致的。
- 定时对账兜底:任何消息链路都可能出现极端问题,所以每天晚上跑一次对账任务,拉取订单、支付、库存流水进行比对,发现不一致就触发补偿流程。
后来在某些高一致性场景,比如库存扣减,我们用了 Redis 加 Lua 脚本进行预扣减,再异步同步到 MySQL。这里必须想清楚一个原则:宁可对账复杂一点,也不能让主流程因为分布式事务锁而拖垮。
2.4 高可用与弹性设计
生产级系统的另一个重点是高可用。Kubernetes 里的多副本是基础,但副本越多不代表越稳定,还要考虑故障隔离和自动伸缩。我们给每个服务配置了 HPA,比如订单服务 CPU 超过 70% 时自动扩容,副本数范围是 3 到 10,缩容也设置了冷却时间,避免流量抖动导致频繁扩缩容。
资源层面,每个容器的request和limit必须写清楚,防止某个服务把节点内存打爆。探针也要区分存活和就绪:存活探针失败时重启容器,就绪探针失败时摘除流量。服务调用侧统一设置超时和重试,但必须加熔断器,比如 Sentienl 的熔断规则,否则重试会变成雪崩助推器。我们还配置了 Pod 反亲和,让同一个服务的多个副本尽量分布在不同的节点上,避免单节点故障导致所有副本一起挂。
3. 技术栈选型,每一项都是取舍
到了技术选型这一步,最容易翻车的就是“什么热选什么”。正确的思路是先明确需求,再列出候选,最后根据团队能力、运维成本、社区生态拍板。我按实际项目的选型过程逐一讲。
3.1 服务框架与生态
服务框架基本没有悬念,我们选了 Spring Boot 3.2.x 加 Spring Cloud Alibaba。为什么不是 Spring Cloud Kubernetes?不是因为后者不好,而是它把注册发现、配置管理交给 Kubernetes 原生的 Service、ConfigMap,抽象粒度太粗,对于 Java 微服务团队反而不好做细粒度控制。为什么不用 Go 或其它语言重写?团队现有积累全在 Java,业务复杂度高,重写风险太大。
Spring Boot 3 的启动速度和内存占用比 2.x 有明显提升,加上 Spring Cloud Alibaba 对 Nacos、Sentinel、Seata 的一体化集成,能少写很多胶水代码。这里建议选型时写清楚版本矩阵:Spring Boot 版本、Spring Cloud 版本、Spring Cloud Alibaba 版本必须严格对应,直接采用官方推荐的版本组合,不要自己随便升级,否则会遇到各种兼容性坑。
3.2 注册中心、配置中心与网关
注册中心选型时对比了 Nacos、Eureka、Consul。Eureka 2.x 基本不再演进,首先排除;Consul 是 CP 系统,强一致但性能上限偏保守,而且配置中心的体验不如 Nacos;Nacos 2.x 支持 AP 和 CP 两种模式,服务发现用 AP,配置中心自带,这样我们不用再单独维护注册中心和配置中心两套系统。实际使用中 Nacos 的命名空间和 group 设计在多环境隔离下非常方便,配置按服务名-环境.yaml动态下发,改动后不用重启服务。
网关我们对比了 Spring Cloud Gateway 和 APISIX。APISIX 基于 OpenResty,性能高,但引入的是 Lua/Go 插件体系,团队短时间内难以上手。Spring Cloud Gateway 和微服务生态一致,基于 WebFlux 写路由和过滤器很顺手,于是选了它。Kubernetes Ingress 继续负责域名和证书,Gateway 只做业务路由,两者分工明确。如果团队对性能有极端要求,再考虑替换 APISIX,但对大多数业务系统来说,Gateway 的表现已经足够了。
3.3 消息、缓存、数据库
消息队列选型是项目里讨论最多的一项。RocketMQ、Kafka、RabbitMQ 都能用,但侧重点完全不同。RabbitMQ 足够轻量,但顺序消息和事务消息支持不够;Kafka 吞吐很高,但运维复杂度高,而且事务消息需要额外引入 read-only 表来补偿,麻烦;RocketMQ 原生支持事务消息、延迟消息、顺序消息,对这个项目几乎全是加分项,所以最终选 RocketMQ。如果你所在团队没有运维开源中间件的经验,更建议用云厂商托管版,把运维风险降到外部。
缓存和数据库方面:Redis 7 集群承担缓存、分布式锁、库存预扣减;MySQL 8.0 作为主力数据库,订单表后续按用户 ID 分库分表,使用 ShardingSphere;商品搜索用 Elasticsearch,而不是直接在 MySQL 里LIKE;通知记录和审计日志写入 MongoDB,这类数据不需要强事务,列式扩展体验好。选型时注意一点:不要为了一种数据库而造业务场景,只有数据访问模式明确后才引入新组件。
3.4 可观测性、CI/CD 与安全
生产级系统必须把可观测性当成功能来做。监控使用 Prometheus 抓取指标,Grafana 做可视化,Alertmanager 负责告警;日志使用 Loki,统一采集到对象存储,通过 Grafana 查询;链路追踪使用 Micrometer Tracing 加 Zipkin,所有服务在网关入口生成 traceId,通过 Feign 拦截器向下游传递。这三个数据源最好在同一个 Grafana 里联动,排查问题才高效。
CI/CD 上,我们是 GitLab CI 触发构建,采用多阶段 Dockerfile 构建镜像,推送私有镜像仓库,随后 Helm 部署到 Kubernetes。每个环境独立命名空间,配置通过 Nacos 下发,不在镜像里写死。安全方面,网关统一做 JWT 校验,服务间使用内网通信,关键敏感数据加密存储。服务间 mTLS 我们评估后没有直接上服务网格,因为维护成本高,初期通过内网和网络策略控制也能兜住安全底线。
4. 核心服务落地实战:以订单服务为例
讲完设计和选型,我以订单服务为例,把从代码初始化到 Kubernetes 部署的完整过程跑一遍。这个流程能覆盖大部分微服务的落地套路,照着做可以少走很多弯路。
4.1 初始化工程与目录结构
订单服务使用 Spring Initializr 创建,依赖包括spring-boot-starter-web、spring-cloud-starter-openfeign、spring-cloud-starter-alibaba-nacos-discovery、spring-cloud-starter-alibaba-nacos-config、spring-boot-starter-actuator、MySQL 驱动、MyBatis-Plus。要注意的是,Spring Cloud 2023 之后不少配置项需要引入spring-cloud-starter-bootstrap,否则外置配置读取会失效。
典型的目录结构如下:
order-service ├── src/main/java │ └── com/company/order │ ├── controller // 接收HTTP请求 │ ├── service // 业务逻辑 │ ├── dao // 数据访问层 │ ├── domain // 领域模型 │ ├── mq // RocketMQ生产者消费者 │ ├── feign // 远程调用 │ └── config // 配置类 ├── src/main/resources │ ├── bootstrap.yml // Nacos优先加载的配置 │ └── application.yml ├── Dockerfile └── helm/ // 部署模板bootstrap.yml是一个非常容易踩坑的地方。它负责提前连接 Nacos,再拉取order-service.yaml配置文件。很多问题比如“启动报错找不到配置”、“配置修改不生效”,八成都是bootstrap.yml里的 namespace、group、dataId 写错了。
4.2 核心业务链路与代码实现
订单服务最核心的下单逻辑,我们把它拆成三步:幂等校验、创建订单、发布事件。核心代码长这样:
@PostMapping("/api/v1/order") public Result<Long> createOrder(@RequestBody CreateOrderCommand cmd, @RequestHeader("X-Request-ID") String requestId) { // 幂等校验:Redis中存在requestId直接返回已有结果 if (idempotentService.check(requestId)) { return Result.success(idempotentService.getResult(requestId)); } // 同一个本地事务里:写订单表 + 写订单事件表 Long orderId = txTemplate.execute(() -> { OrderEntity order = orderService.createOrder(cmd); orderEventService.savePendingEvent(order.getId(), OrderCreatedEvent.TYPE); idempotentService.set(requestId, order.getId()); return order.getId(); }); // 事务提交后,发送RocketMQ事务消息,消费端是库存服务、履约服务 rocketMqTemplate.sendMessageInTransaction("order_topic", new OrderCreatedEvent(orderId, cmd.getUserId(), cmd.getItems()), null); return Result.success(orderId); }这里最容易被忽略的是“事务消息”的用法:先本地事务写业务数据和事件记录,再发送事务消息,RocketMQ 会回调检查本地事务状态来决定是否最终提交。这样才能保证“用户看到的订单一定创建成功,下游一定会收到消息”。
4.3 配置中心、限流与熔断
订单服务在 Nacos 里的配置大概是这样的:
server: port: 8080 order: timeout: 30 auto-close-minutes: 30 spring: redis: cluster: nodes: redis-1:7001,redis-2:7002,redis-3:7003页面或控制台修改配置后,服务会自动通过 Nacos 长轮询感知并刷新。但注意:配置要生效,类必须加上@RefreshScope,不加就是白搭。很多同学说配置中心没用,最后发现是忘了加注解,这个问题我们在 5.3 节会重点说。
限流我们通过 Sentinel 网关和控制台配合。网关层配置/api/v1/order/**的限流规则,针对用户维度做 QPS 限制,超过阈值直接返回友好错误。核心服务内部,对库存服务的 Feign 调用加了熔断规则,当请求慢调用比例超过 30% 时触发熔断,并降级返回预生成提示。
4.4 容器化与 Kubernetes 部署
Dockerfile 采用多阶段构建,避免把 Maven 依赖打进运行镜像:
FROM maven:3.9-eclipse-temurin-17 AS builder COPY . /work WORKDIR /work RUN mvn -q package -DskipTests FROM eclipse-temurin:17-jre RUN useradd --create-home appuser USER appuser COPY --from=builder /work/target/order-service.jar /app/order-service.jar ENTRYPOINT ["java", "-jar", "/app/order-service.jar"]Kubernetes 的 Deployment 配置里,我最关心的是三部分:探针、资源、启动参数。
apiVersion: apps/v1 kind: Deployment metadata: name: order-service namespace: prod spec: replicas: 3 strategy: rollingUpdate: maxUnavailable: 0 maxSurge: 1 selector: matchLabels: app: order-service template: metadata: labels: app: order-service spec: containers: - name: order-service image: registry.internal/order-service:1.4.2 ports: - containerPort: 8080 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 20 periodSeconds: 10 livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 60 periodSeconds: 20 resources: requests: cpu: 500m memory: 512Mi limits: cpu: "2" memory: 1Gi env: - name: JAVA_OPTS value: "-XX:MaxRAMPercentage=80 -Djava.security.egd=file:/dev/./urandom"MaxRAMPercentage一定注意:当容器有内存 limit 时,JVM 默认是按照宿主机内存来计算堆大小,不设置这个参数很容易让容器因内存超限被杀,这是 K8s 部署 Java 应用最常见的坑。
4.5 网关路由与全链路联调
网关服务里加一段路由配置:
spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path=/api/v1/order/** filters: - StripPrefix=1 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 100 redis-rate-limiter.burstCapacity: 200lb://order-service表示从 Nacos 按服务名负载均衡,不需要写死 IP。StripPrefix=1会把/api去掉,这样网关把/api/v1/order转发到订单服务后,订单服务收到的是/v1/order,路由不冲突。
联调验证的命令我习惯这样跑:
curl -X POST 'https://api.internal.example.com/api/v1/order' \ -H 'Content-Type: application/json' \ -H 'X-Request-ID: 20240615-00001' \ -d '{"user_id":1001,"items":[{"sku_id":2001,"quantity":1}]}' \ -v返回结果里一定要能看到trace_id。之后在 Zipkin 页面上按 traceId 搜索,可以看到网关、订单服务、库存服务的完整调用链。如果链路断在某个节点,优先检查 Feign 拦截器是否把 traceId 往下传了。
5. 生产落地中的典型问题与排查清单
再好的设计,落到生产环境都会遇到新问题。这一章我把项目里真实踩过的几个高频问题整理出来,按“现象、排查思路、解决方法”的顺序写,方便大家直接对照。
5.1 服务反复重启与探针失败
现象是 Pod 起来了但马上进入CrashLoopBackOff,或者Readiness一直不通过。大部分原因是依赖没有就绪,比如 MySQL 还没连上、Nacos 连接超时,或者健康检查配置错误。不要一上来就盲目调探针,先用kubectl describe pod和kubectl logs看事件和应用日志。如果日志显示 MySQL 连接慢,就调大initialDelaySeconds,同时把启动命令的-Djava.security.egd加上,避免随机数阻塞拖慢启动。
5.2 Nacos注册正常但Feign调用失败
很多同学在 Nacos 控制台能看到服务列表,但服务之间调用报“No provider”。这个问题绝大多数是命名空间或 group 不一致。比如订单服务在 dev 命名空间,库存服务在 prod 命名空间,Feign 当然找不到。排查时先在 Nacos 控制台逐个核对服务所在命名空间和 group,再看@FeignClient(name = "inventory-service")里的名字是否和application.yml的spring.application.name完全一致,大小写不要忽略,因为默认是精确匹配。
5.3 配置中心动态刷新失效
配置中心改完值,服务没变化,这是非常打击信心的问题。我把它拆成三个检查点:一是配置是否真的加载到了 Nacos,服务启动日志里有 Config 拉取记录;二是bootstrap.yml不受application.yml影响,如果配置写在application.yml里,Nacos 根本管不到;三是目标 Bean 有没有@RefreshScope。要注意的是,@RefreshScope应该加在配置属性类的 Bean 上,而不是加在使用这个配置的 Service 类上,否则刷新后实例不会重新构造,配置还是旧的。
5.4 分布式事务与消息重复消费
事务消息能解决“本地事务和发消息不一致”,但不能解决消费端重复消费。RocketMQ 的 at-least-once 语义决定了消费者必须自己做幂等。我们在订单服务和库存服务里都加了消费幂等表,用唯一键event_id + service_name作为主键,重复消息来了直接跳过。扣减库存时还要注意并发,我们在 Redis 里用 Lua 脚本预扣减,脚本里校验库存是否充足,不足则拒绝,避免超卖。
5.5 链路追踪与日志串联不上
网关层的 traceId 能生成,但调用链到某个服务就断了,通常是因为 Feign 内部没有传递 traceId。Spring Boot 3 使用 Micrometer Tracing,它会自动处理常见的 Feign、RestTemplate,但要确认所有依赖都是同一个版本体系,比如micrometer-tracing-bridge-zipkin。如果还不行,就手动加一个 Feign RequestInterceptor,从上层 context 取出 traceId,放进 header。日志方面,建议所有服务统一用 JSON 格式输出,Grafana 或 Kibana 中直接按 traceId 字段过滤,这样排查效率会提高很多。
5.6 快速排查速查表
| 症状 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 服务反复重启,Readiness 失败 | 依赖未就绪或探针路径错误 | kubectl describe pod、查看应用日志 | 调整探针延迟、检查依赖连接 |
| Feign 调用 No provider | Nacos 命名空间/group 不一致 | Nacos 控制台核对服务注册信息 | 统一 namespace、group、服务名 |
| Nacos 配置更新不生效 | 缺少 @RefreshScope 或配置位置不对 | 检查启动日志、加载源 | 修正配置加载方式和注解 |
| 消息重复执行同一业务 | 消费端未做幂等 | 查消费日志、Redis 唯一键 | 增加幂等表和去重逻辑 |
| 链路追踪 traceId 断开 | Feign 未传 traceId、依赖版本不一致 | 查各服务日志 trace_id | 统一版本、手动传递 trace_id |
| JVM 频繁 OOM 或容器被杀 | 未设置 MaxRAMPercentage | 看 K8s events、监控内存 | 使用 -XX:MaxRAMPercentage=80 |
5.7 经验教训:如果再让我做一遍
复盘完这个项目,我最想提醒三点。第一,技术栈选型一定要把版本矩阵固定下来,最好一开始就构建一个最小的“模拟链路”:一个服务注册到 Nacos,通过网关路由到另一个服务,链路追踪贯通,再开始铺业务。这个骨架跑通后再加业务代码,可以避免后期排查一堆环境问题。
第二,可观测性必须和非功能需求同步设计,而不是在项目快上线时补。我最后悔的就是监控指标定义得太晚,导致前面一段时间出了问题只能靠日志慢慢翻。先确定核心服务的 RED 指标,再写业务功能,后面的排查成本会低一个量级。
第三,不要为了微服务而微服务。我们这个项目之所以拆得顺利,是因为业务需求确实复杂、并发确实增长,团队协作确实有痛点。如果只是“觉得微服务高级”就去拆,最后只会得到一套分布式单体的复杂版本。任何时候,架构决策都应该先回答“为什么”,再回答“怎么做”。