前端学 Spring Boot(10):一个应用拆成十个服务,问题会变少吗?

📅 2026/7/29 9:09:33
前端学 Spring Boot(10):一个应用拆成十个服务,问题会变少吗?
用户在结算页点击“提交订单”。前端只发出一次请求awaitaxios.post(/api/orders,order)在最初的项目里这个请求进入一个 Spring Boot 应用。它检查商品、扣减库存、创建订单再返回订单号。所有代码在同一个项目里出问题时也可以沿着一份日志往下找。后来业务变大系统被拆成用户服务、商品服务、订单服务、库存服务和支付服务。前端仍然只点了一次按钮后端却要让五个独立程序一起完成工作。微服务就是从这里开始变难的原来同一个应用里的几次方法调用变成了几个程序隔着网络互相请求。先把单体想成一家小餐馆一家小餐馆里点单、做菜、收银和打包都在同一个店面。店员转身说一句“3 号桌不要辣”厨房马上就能听见。这很像单体应用。Controller、Service、数据库访问代码虽然职责不同但运行在同一个 Java 进程里一个 Spring Boot 应用 ├── 用户模块 ├── 订单模块 ├── 库存模块 └── 支付模块“单体”不等于“混乱”。只要模块边界清楚一个应用同样可以很好地维护、测试和部署。真正的问题通常出现在规模扩大以后几百名员工挤在一家店里改一张菜单要等所有人确认厨房繁忙时只能把整家店一起扩建一处停电还会让所有业务同时停止。微服务像把餐馆变成多个专业门店系统拆分后每个服务像一家独立门店订单服务只管理订单库存服务只管理库存支付服务只处理支付和退款通知服务只负责短信和站内消息。它们可以由不同团队维护分别发布也可以单独扩容。大促时库存查询压力很高只增加库存服务的实例不必把通知服务也复制十份。代价是店员不能再转身说话。他们需要打电话、发消息还要确认对方是否收到。因此微服务并不是把一个项目复制十份再分别改端口。真正的变化是每个服务都能独立运行和发布也必须为自己的业务和数据负责。应该按“谁负责这件事”来拆假设按代码类型拆成 Controller 服务、Service 服务和数据库服务一次业务请求仍然必须依次经过它们。改一个订单功能三个服务都要修改和发布。这只是把原来的调用链搬到了网络上复杂度增加了独立性却没有增加。更自然的拆法是问这件事最终由谁负责订单能不能取消由订单服务负责商品还剩多少由库存服务负责钱有没有付成功由支付服务负责用户能做什么由用户与权限系统负责。这类围绕一项完整业务职责划出的范围叫服务边界。边界不可能只看数据库表名决定。订单里可以保存商品名称和成交价因为订单需要记录“购买当时发生了什么”但商品当前售价仍然由商品服务负责。如果团队暂时说不清谁负责什么先整理单体内部的模块往往比急着拆服务更稳妥。Java 方法变成 HTTP 后事情不再确定在一个应用里订单模块预留库存可能只是一次普通调用stockService.reserve(productId,quantity);拆成两个服务后它可能变成一次 HTTP 请求POST /internal/stock/reservations Content-Type: application/json {productId: 42, quantity: 1}方法调用要么返回要么抛异常。网络调用却可能出现一种很麻烦的情况订单服务没有收到响应但它不知道库存服务究竟有没有执行成功。这就像打电话让同事锁定一件商品刚说完电话断了。你不知道对方没听见还是已经办完但来不及回答。如果订单服务直接再请求一次库存可能被扣两次。因此上一篇介绍的幂等键在微服务中尤其重要同一次业务意图即使送达多次也只能产生一次结果。服务地址不能写死在代码里本地开发时库存服务可能运行在http://localhost:8082生产环境不会这么固定。库存服务可能同时运行三个实例扩容后变成五个某个实例故障后又只剩四个。每次发布时实例地址也可能变化。系统需要一份不断更新的“通讯录”记录某个服务当前有哪些可用实例。调用方通过服务名找到地址再从可用实例中选择一个订单服务库存服务通讯录库存实例 A库存实例 B库存实例 C这套寻找服务地址的机制叫服务发现把请求分给多个实例叫负载均衡。在 Kubernetes 中Service 和集群 DNS 可以提供这类能力其他环境也可能使用注册中心。工具可以不同目标都是让订单服务只关心“我要找库存服务”而不是记住某台服务器的 IP。API 网关像大楼前台如果前端需要分别记住用户、商品、订单和支付服务的地址登录信息、跨域配置和错误处理都会变得混乱。更常见的做法是在最外层设置一个统一入口也就是 API 网关Web / AppAPI 网关用户服务订单服务商品服务前端仍然访问/api/orders网关负责把请求转给订单服务。它还可以统一处理登录检查、限流和访问日志。但网关更像前台不是老板。它可以告诉请求去几楼却不应该决定订单是否允许退款。真正的业务规则仍然属于对应服务否则网关会逐渐变成另一个难以维护的巨型应用。每个服务都要管好自己的账本在单体应用里多个模块经常读取同一个数据库。拆分以后如果订单服务仍然直接修改库存表库存服务就失去了对库存的控制。更清晰的原则是谁负责业务谁拥有相关数据。订单服务 → 订单数据 库存服务 → 库存数据 支付服务 → 支付数据订单服务需要扣库存时应该请求库存服务而不是绕过它直接执行 SQL。这样库存服务才能统一处理“不能扣成负数”“重复请求不能多扣”等规则。这里的独立强调数据所有权不一定意味着每个服务都要购买一台数据库服务器。它们可以使用同一个数据库集群但不能把彼此的表当成公共变量随意修改。这也带来一个明显变化以前一条 JOIN 能查完的数据现在可能分散在几个服务里。订单详情页要展示订单、商品和支付状态可以在接口层组合多个服务的结果也可以提前同步必要数据生成一份适合查询的订单视图。选择哪种方式要看数据需要多新、请求量有多大以及某个服务暂时不可用时页面是否还能展示。一次下单不再能用一个事务包住单体应用可以用Transactional保证创建订单和扣库存一起成功、一起回滚。拆分后订单与库存属于两个程序、两个数据库。订单数据库无法命令库存数据库跟着自己回滚。系统只能把下单拆成多个有状态的步骤失败失败创建待确认订单预留库存发起支付确认订单取消订单释放库存如果支付失败系统需要取消订单并释放库存。这里的“释放库存”不是数据库自动回滚而是一次新的业务操作通常叫补偿。补偿本身也可能失败所以系统要记录当前进行到哪一步、失败了几次、下次何时重试。只把步骤写在一段 Java 代码里并不可靠因为应用可能在任何一行执行后重启。这就是最终一致性的实际含义几个服务不保证在同一瞬间全部完成但系统知道过程处于什么状态并会通过重试或补偿让它最终走向明确结果。消息队列像服务之间的待办箱订单创建后还要发送通知、增加积分和更新统计。如果订单服务逐个等待这些工作任何一个服务变慢用户都要一直看着 Loading。订单服务可以发出一条“订单已创建”消息然后先返回结果订单服务消息队列通知服务积分服务统计服务消息队列像一个可靠的待办箱。生产者把任务放进去消费者按照自己的处理能力取走。短时间出现大量订单时消息可以先排队不必同时压向所有服务。但“放进待办箱”不等于事情已经完成。消息可能重复投递消费者可能处理失败长期失败的消息也需要告警。异步只是让用户请求不用原地等待并没有取消失败处理。一个慢服务可能拖住整条链路订单等待库存库存等待商品商品又在等待数据库。最底层慢五秒上面的服务就可能一起等五秒。如果每个请求都一直等待线程和连接很快会被占满原本正常的接口也开始超时。这像一个窗口办事太慢队伍一路排到其他窗口门口最后整座大厅都无法工作。远程调用至少要先设置超时超过合理时间后停止等待释放当前资源。其他常见保护手段也可以用日常动作理解重试确认是短暂问题后再打一次电话退避不要马上连续拨打等待一段时间再试熔断对方已经持续故障暂时停止拨打隔离一个窗口的长队不能占满所有窗口降级推荐暂时不可用仍然返回订单主体信息。这些机制不是越多越好。支付请求能不能重试、订单最多等待多久、哪些数据允许缺失都必须根据业务决定。尤其要记住重试会制造重复请求。只有接口已经具备幂等能力或者明确知道第一次没有执行重试才是安全的。服务升级时不能要求大家同时改完前端开发者很熟悉组件 Props 的变化一个被几十个页面使用的组件不能随意删除属性并期待所有页面同一秒更新。服务接口也是一样。订单服务增加新字段时旧版调用方可能仍在运行滚动发布期间同一个服务的新旧实例也会同时存在。比较安全的演进方式包括新字段先设计为可选旧调用方可以忽略不随意改变原字段的含义删除字段前先确认没有调用方继续使用重大变化保留一段新旧版本并存的时间。OpenAPI 不只是生成接口文档也可以让双方检查接口结构。自动化测试还应该验证调用方发送的请求提供方是否真的接受提供方返回的内容调用方是否真的看得懂。服务能够独立发布的前提是接口变化能够向前兼容。否则十个服务仍然必须约好同一晚一起上线只是把一个发布变成了十个发布。traceId 是一次请求的快递单号在单体应用中一次请求的日志通常都在一个地方。微服务中同一次下单可能经过网关、订单、库存和支付服务每个服务都有自己的日志。如果只知道用户“下午三点左右下过单”排查问题会变成在几百万行日志里碰运气。系统可以在请求进入时生成一个 traceId并让它跟着请求一路传递网关 traceIdabc 订单服务 traceIdabc 库存服务 traceIdabc 支付服务 traceIdabc它像快递单号把不同地点发生的记录串成同一次旅程。链路追踪还会显示每一站用了多久、调用了谁、在哪里报错。服务越多统一日志、指标、traceId 和告警越重要。否则一个错误只会得到四句话“前端发了”“网关转了”“订单调了”“库存没看到”。微服务把代码拆小也把运维对象变多一个应用变成十个服务后需要管理十份构建、镜像、配置、发布、健康检查、监控和权限。开发一个功能时本地可能还要准备多个依赖服务。容器和 Kubernetes 可以帮助启动、扩容和替换实例但它们不会自动找出正确的业务边界也不会替系统处理重复请求和数据一致性。微服务真正适合解决的问题通常很具体团队之间经常因为发布互相等待某个业务需要独立快速迭代部分功能的流量远高于其他功能单个应用已经大到难以理解和修改故障需要被限制在更小范围内。如果团队只有几个人业务边界还在频繁变化单体也能快速稳定发布那么拆分很可能只会增加网络和运维成本。一个结构清楚的模块化单体并不落后。更稳妥的路线通常是先在一个应用里把职责分清再根据真实的协作、扩容和故障问题逐个拆出确实需要独立的模块。当一个应用拆成十个服务问题不会自动变少。原来要处理的是类与模块之间的关系现在还要处理网络、数据、版本和故障之间的关系。微服务也不是 Spring Boot 项目的毕业证。能说清楚为什么要拆、按什么边界拆以及愿意为这份独立性付出什么成本才说明系统真的需要它。