从单体应用到微服务:后端系统拆分的基本方法

📅 发布时间:2026/9/6 4:41:05
从单体应用到微服务:后端系统拆分的基本方法
摘要微服务并不是把一个项目简单拆成几个 Spring Boot 应用。真正的微服务改造需要重新设计业务边界、数据所有权、服务通信、部署方式、故障处理和团队协作模式。单体应用在早期通常开发效率高、部署简单、事务边界清晰但随着业务增长代码耦合、发布风险、团队协作和系统扩展会逐渐变得困难。微服务可以让不同业务能力独立演进却也会引入网络延迟、数据一致性、分布式事务、链路追踪和运维复杂度。本文从一个包含用户、商品、订单和支付功能的单体系统出发介绍微服务拆分前的判断方法、领域边界设计、数据库拆分、同步与异步通信、迁移步骤和治理能力。读完本文后你应该能够判断一个单体应用是否真的需要拆分使用业务边界而不是代码数量设计服务规划服务之间的调用和数据所有权避免共享数据库和分布式事务带来的常见问题设计从单体到微服务的渐进式迁移路径了解微服务上线后必须补齐的治理能力。一、背景与问题1. 单体应用是什么单体应用通常将多个业务模块打包成一个可部署单元一个代码仓库 - 一个构建产物 - 一个进程 - 一个数据库在电商系统中用户、商品、订单、库存和支付可能都在同一个应用内Web 层 - 用户模块 - 商品模块 - 订单模块 - 库存模块 - 支付模块 - 数据访问层 - 一个数据库单体不代表设计差。模块边界清晰、团队规模较小、系统规模适中的单体应用往往比过早拆分的微服务更容易开发和运维。2. 单体应用什么时候开始出现问题常见信号包括修改一个小功能需要重新发布整个系统不同团队频繁修改同一批代码某个模块的流量增长拖慢所有模块应用启动和回归测试耗时越来越长数据库表被多个模块随意读写一个模块升级依赖导致其他模块受到影响故障影响范围无法隔离不同业务需要不同的扩容策略发布窗口和回滚风险持续增加。这些信号说明系统的边界可能已经不适合当前规模但不一定意味着必须立即微服务化。也可以先通过模块化、代码分层、数据库治理和发布优化解决一部分问题。3. 微服务解决什么问题微服务通常希望实现业务能力独立 - 服务独立构建 - 服务独立部署 - 服务独立扩容 - 服务故障隔离 - 团队独立负责例如用户服务 商品服务 订单服务 库存服务 支付服务 通知服务每个服务围绕一个业务能力负责而不是围绕一个技术组件随意切分。4. 微服务带来的新问题拆分以后原来的进程内调用变成网络调用单体 订单模块 - 库存模块 直接方法调用 微服务 订单服务 - 网络 - 库存服务随之而来的问题包括网络可能超时或失败服务之间有延迟请求可能重复到达数据不再共享一个事务服务发现和配置变复杂日志需要跨服务关联部署和监控对象数量增加一个请求可能依赖多个下游服务。微服务的价值必须大于它引入的复杂度不能把拆分本身当成目标。二、核心概念1. 服务边界服务边界是一个业务能力的责任边界。它应该回答这个服务负责什么业务规则哪些数据归它所有哪些操作由它决定其他服务如何请求它它需要发布什么事件它对外承诺什么接口。一个好的服务边界通常具有高内聚相关业务规则和数据放在一起低耦合跨服务依赖尽量少独立演进可以单独发布和扩展清晰所有权某类数据只有一个权威服务。2. 按业务能力拆分而不是按技术层拆分不推荐按技术层拆分用户 Controller 服务 订单 Controller 服务 公共 Service 服务 数据库 Service 服务这种拆分会让一次业务请求跨越很多技术服务反而增加耦合。更合理的是按业务能力拆分用户服务用户资料、账户状态 商品服务商品信息、价格、上下架 订单服务订单生命周期和订单金额 库存服务库存数量和扣减规则 支付服务支付单和支付状态每个服务内部仍然可以有 Controller、Service 和 Repository但服务边界应首先由业务职责决定。3. 领域驱动设计中的限界上下文限界上下文可以理解为一组拥有明确模型和规则的业务边界。同一个词在不同上下文中可能有不同含义概念用户上下文订单上下文支付上下文用户账户和身份下单人快照付款人商品收藏对象购买条目支付描述状态账户状态订单状态支付状态金额账户额度订单应付金额实际支付金额拆分时不能因为多个模块都使用“用户”这个词就让所有服务共享同一个 User Entity。服务可以保存自己需要的用户快照通过用户服务接口或事件获取必要信息。4. 数据所有权微服务中的核心原则是一个业务数据有一个权威拥有者 其他服务通过 API、事件或只读副本获取数据例如商品服务拥有商品名称和商品状态库存服务拥有可售库存订单服务拥有订单状态和订单金额支付服务拥有支付状态用户服务拥有账户基础信息。其他服务可以保存必要的快照但不能绕过拥有者直接修改原始数据。5. 服务通信方式服务之间常见两类通信方式特点适用场景同步 HTTP/RPC调用关系直观实时返回查询、必须立即得到结果异步消息解耦、削峰、最终一致领域事件、通知、耗时任务批量接口减少网络往返列表查询和批量处理本地缓存降低重复调用稳定、可短暂过期的数据选择通信方式时要看业务是否必须同步得到结果。不是所有跨服务调用都应该使用同步 HTTP。6. API 契约服务之间通信需要稳定契约请求路径和方法 请求参数和响应字段 状态码和错误码 超时和重试约定 幂等要求 权限要求 版本兼容策略API 契约不是 Controller 方法签名的简单复制。服务之间应该隐藏内部实体和数据库结构只暴露稳定的业务接口。7. 事件驱动事件表示某件已经发生的业务事实OrderCreated PaymentSucceeded StockDeducted OrderCancelled事件的特点过去发生的事实不是命令生产者不必知道所有消费者消费者可以异步处理适合传播状态变化需要考虑重复消费和顺序。事件不是万能的。对必须立即得到结果的校验仍然需要同步调用。8. 分布式事务与最终一致性单体中可以使用一个数据库事务创建订单 - 扣减库存 - 创建支付单 - 一个数据库事务提交拆分后不同服务通常拥有不同数据库无法简单使用同一个本地事务。常见方案包括可靠事件和最终一致性本地消息表Saga 编排或协同补偿事务TCC尽量避免跨服务强一致事务。方案选择取决于业务能否接受短暂中间状态、补偿是否可行以及失败后的人工处理成本。三、工作原理1. 单体到微服务的演进一个渐进式演进过程可以是模块化单体识别业务边界稳定内部接口抽离一个低风险服务建立服务通信和观测迁移数据所有权逐步拆分其他能力不建议一次性把所有模块拆成几十个服务。每拆一个服务都应该获得明确收益并能验证迁移是否成功。2. 服务边界示例假设原单体中包含以下模块UserModule ProductModule OrderModule InventoryModule PaymentModule NotificationModule可以先形成候选边界用户服务 - 用户资料、账户状态、用户认证关联 商品服务 - 商品信息、价格、上下架 库存服务 - 库存、预占、释放、扣减 订单服务 - 订单创建、状态流转、订单查询 支付服务 - 支付单、支付状态、支付回调 通知服务 - 短信、邮件、站内通知候选边界还需要结合团队职责、数据依赖和发布频率验证。3. 从同步调用到异步事件订单创建可能经历通知服务支付服务库存服务订单服务客户端通知服务支付服务库存服务订单服务客户端创建订单请求预占库存返回预占结果创建支付单返回支付单返回待支付订单发布支付成功事件发布订单状态变化事件发送通知这里可以把支付成功后的通知改成异步事件让订单服务不必同步等待通知服务完成。4. 数据迁移的核心流程从共享数据库迁移到服务独立数据库不能只复制表梳理表和字段所有者 - 确定服务边界 - 建立服务内部模型 - 迁移历史数据 - 建立双写或变更同步 - 校验新旧数据 - 切换读取 - 停止旧写入 - 删除共享访问双写期间需要处理一边写成功、一边写失败两边写入顺序不一致重复写入数据格式不同历史数据缺失补偿和校验。如果无法可靠保证双写一致应优先采用事件同步或分阶段迁移避免长期维护不可控的双写逻辑。5. 服务调用的超时与重试一次同步调用至少要明确连接超时 读取超时 总调用超时 最大重试次数 退避时间 是否允许重试 降级结果不是所有失败都适合重试查询通常比较容易重试创建操作必须具备幂等键后才能重试支付请求需要严格遵循业务幂等超时不代表服务端没有执行成功重试可能放大下游压力。6. 服务故障的传播如果订单服务同步依赖库存、优惠、地址和支付服务订单请求 - 库存慢 - 订单线程等待 - 优惠慢 - 更多线程等待 - 连接池耗尽 - 订单服务超时 - 上游重试 - 故障扩大微服务必须配合超时限流熔断舱壁隔离降级异步化依赖分级资源池隔离。四、实战示例下面以订单服务调用库存服务为例展示 API 契约、幂等、事件和补偿设计。1. 定义服务接口库存服务可以提供预占接口POST /api/inventories/reservations Idempotency-Key: order-10001-create { orderId: 10001, items: [ { productId: 10, quantity: 2 } ] }响应{reservationId:reservation-90001,status:RESERVED,expiresAt:2026-09-05T12:00:00Z}接口契约需要明确orderId 是否必须唯一重复请求返回什么库存不足返回什么错误码预占多久自动过期如何释放预占订单取消后谁负责触发释放。2. 订单服务调用库存服务ServicepublicclassOrderApplicationService{privatefinalInventoryClientinventoryClient;privatefinalOrderRepositoryorderRepository;publicOrderApplicationService(InventoryClientinventoryClient,OrderRepositoryorderRepository){this.inventoryClientinventoryClient;this.orderRepositoryorderRepository;}TransactionalpublicOrderResultcreateOrder(CreateOrderCommandcommand){OrderorderorderRepository.createPending(command.userId(),command.items());ReservationResultreservationinventoryClient.reserve(newReserveInventoryCommand(order.id(),command.items()),order-order.id());if(!reservation.success()){orderRepository.markFailed(order.id(),INSUFFICIENT_STOCK);returnOrderResult.failed(order.id(),库存不足);}orderRepository.markReserved(order.id(),reservation.reservationId());returnOrderResult.success(order.id());}}这里的 Transactional 只覆盖订单服务自己的数据库不能自动覆盖库存服务。库存调用失败时订单状态需要通过本地事务和明确的业务状态表达。3. 设计客户端超时和幂等ComponentpublicclassInventoryClient{privatefinalRestClientrestClient;publicInventoryClient(RestClient.Builderbuilder){this.restClientbuilder.baseUrl(http://inventory-service).build();}publicReservationResultreserve(ReserveInventoryCommandcommand,StringidempotencyKey){returnrestClient.post().uri(/api/inventories/reservations).header(Idempotency-Key,idempotencyKey).body(command).retrieve().body(ReservationResult.class);}}实际项目还需要在 HTTP 客户端层配置连接超时、读取超时、状态码映射和重试策略。创建类接口必须传递稳定幂等键避免客户端超时重试导致重复预占。4. 库存服务实现幂等库存服务可以保存请求幂等记录idempotency_key service_name request_hash response_body status created_at expires_at处理流程收到请求 - 根据幂等键查记录 - 已成功返回原响应 - 处理中返回处理中状态或等待 - 已失败根据策略重试或返回原错误 - 未找到执行业务并保存结果如果同一个幂等键对应的请求内容发生变化应返回参数冲突而不是静默复用原结果。5. 使用事件完成后续流程订单服务可以发布订单已预占事件{eventId:event-10001,eventType:OrderInventoryReserved,aggregateId:10001,occurredAt:2026-09-05T12:00:00Z,payload:{orderId:10001,reservationId:reservation-90001}}支付服务、通知服务或履约服务可以订阅这个事件。事件消费者必须支持重复消费ComponentpublicclassInventoryReservedHandler{privatefinalEventRecordRepositoryeventRecordRepository;privatefinalPaymentServicepaymentService;Transactionalpublicvoidhandle(OrderInventoryReservedevent){if(eventRecordRepository.exists(event.eventId())){return;}paymentService.createPayment(event.orderId(),event.reservationId());eventRecordRepository.markProcessed(event.eventId());}}事件去重记录和业务更新最好放在同一个本地事务中避免业务处理成功但去重记录没有保存。6. 本地消息表如果订单数据库和消息系统之间没有统一事务可以使用本地消息表CREATETABLEoutbox_event(idBIGINTPRIMARYKEYAUTO_INCREMENT,event_idVARCHAR(64)NOTNULLUNIQUE,event_typeVARCHAR(100)NOTNULL,aggregate_idVARCHAR(64)NOTNULL,payload JSONNOTNULL,statusVARCHAR(20)NOTNULL,retry_countINTNOTNULLDEFAULT0,next_retry_atDATETIMENULL,created_atDATETIMENOTNULL,published_atDATETIMENULL);订单创建时本地事务 - 保存订单 - 保存 outbox_event - 一起提交后台发布器查询待发布事件 - 发送到消息系统 - 标记已发布 - 失败则增加重试次数消息可能重复发布因此消费者必须幂等。Outbox 解决的是“业务数据和待发布事件不一致”的问题不会自动解决所有分布式事务问题。7. 统一错误响应服务间错误响应应包含稳定的错误码{code:INVENTORY_NOT_ENOUGH,message:库存不足,requestId:req-202609050001,retryable:false}不要让调用方依赖异常堆栈、数据库错误字符串或可变的 message。retryable 可以帮助调用方区分是否允许重试但最终仍需结合接口幂等性判断。8. 迁移一个模块的步骤以商品服务为例1. 梳理商品模块的表、接口和业务规则 2. 在单体内部建立 ProductFacade 3. 禁止其他模块直接访问商品表 4. 为 ProductFacade 编写契约测试 5. 创建独立商品服务 6. 将商品数据同步到新库 7. 先灰度读取新服务 8. 校验新旧结果 9. 切换写入和数据所有权 10. 删除单体中的旧实现先在单体内部建立边界能够降低后续抽离难度也可以提前发现模块间隐藏耦合。五、常见问题与实践建议1. 是否所有系统都适合微服务不一定。以下情况可以优先保持模块化单体团队规模较小业务还在快速试错访问规模不大模块边界尚未稳定没有完善的部署和监控能力事务一致性要求很高服务拆分收益不明确。微服务不是成熟度徽章。能够稳定交付和快速演进比服务数量更多更重要。2. 服务是不是越小越好服务过小会导致调用链变长网络开销增加数据一致性更复杂部署单元数量膨胀团队需要维护更多接口故障定位困难。合理边界应该让服务内部有足够业务内聚同时避免一个业务动作跨越过多服务。拆分粒度要结合团队和系统规模动态调整。3. 为什么共享数据库会破坏微服务边界如果多个服务直接访问同一张表订单服务直接修改库存表 商品服务直接修改订单表 支付服务直接查询订单内部字段会导致数据所有权不清晰表结构变更互相影响服务无法独立发布本地事务边界失去意义权限难以控制未来拆库成本很高。过渡阶段可以暂时共享数据库但应通过访问层、表权限和迁移计划限制这种状态不能把它当作长期架构。4. 是否应该为每个服务准备独立数据库独立数据库有利于数据隔离和独立演进但也会增加数据同步分布式事务运维实例备份恢复跨服务查询数据分析整合。可以先做到“逻辑所有权独立”再根据规模和发布需求物理拆库。不要为了形式上的独立数据库而过早承担不必要的复杂度。5. 服务间调用为什么不能直接共享 Entity共享 Entity 会造成一个服务修改字段影响所有调用方内部字段暴露依赖同一版本类库数据模型无法独立演进序列化兼容变复杂。服务之间应使用 DTO、Command、Query 和 Event 等契约对象必要时进行显式映射。6. 重试为什么可能造成重复操作网络超时只说明调用方没有及时收到响应不说明服务端没有执行成功客户端发送创建请求 - 服务端执行成功 - 响应在网络中丢失 - 客户端认为失败并重试 - 服务端再次执行因此创建订单、支付、扣库存等操作必须设计幂等键、业务唯一约束或状态机不能只依赖调用方“不要重复请求”。7. 分布式事务应该怎么选先问业务能否接受中间状态可以接受优先使用事件、状态机和补偿不可接受评估 Saga、TCC 或重新设计边界只涉及同一数据库优先使用本地事务依赖外部系统考虑幂等、对账和人工补偿。不要为了追求“看起来像一个事务”把多个远程调用强行串在长事务中。8. 调用链变长怎么办优化方向合并批量接口使用并行调用将非核心流程异步化增加本地缓存减少跨服务查询设置依赖分级对弱依赖提供降级用聚合服务封装复杂编排。并行调用需要考虑线程池隔离和下游承载能力不能简单地把所有调用都改成并行。9. 如何处理跨服务查询不要让一个服务直接连接另一个服务的数据库。可选方式调用对方查询 API使用事件构建本地只读视图使用数据同步任务由查询聚合服务统一编排对报表场景进入独立分析库。实时一致性和查询性能需要权衡。面向用户的核心交易查询通常优先保持业务边界面向报表的复杂跨域查询可以使用数据同步和分析模型。10. 服务发现和配置中心是不是必需服务数量少、部署环境简单时可以先使用固定地址或平台服务发现。随着实例动态扩缩容和环境增多再引入统一服务发现、配置管理和密钥管理。无论是否使用配置中心都要做到配置与代码分离敏感信息不进代码仓库配置变更可审计配置错误可快速回滚关键配置启动时校验。11. 日志中只看服务本地 requestId 可以吗不够。一次请求可能经过多个服务需要使用贯穿全链路的 traceId网关 - 订单服务 - 库存服务 - 支付服务 - 消息消费者每个服务可以有自己的 spanId但必须传播 traceId。日志、指标和分布式追踪应该能够关联同一次业务请求。12. 微服务部署后如何回滚每个服务应具备可重复构建的版本独立镜像或构建产物健康检查灰度或分批发布数据库迁移回滚策略API 兼容窗口旧版本和新版本并存能力。代码可以快速回滚但数据库结构和消息格式可能已经变化。因此发布前要设计向后兼容避免新版本写入旧版本无法理解的数据。六、进阶思考1. 模块化单体是很好的中间形态模块化单体可以做到一个部署单元 清晰的业务模块 独立的内部接口 明确的数据访问边界 模块级测试 统一但可观察的运行环境它可以先解决代码和责任边界问题同时保留本地调用、单库事务和简单部署的优势。未来只有真正需要独立扩展的模块才抽离为微服务。2. 服务拆分应该由变化驱动一个服务是否应该独立通常看它是否有独立发布频率它是否有独立扩容需求它是否需要不同技术栈它是否由独立团队负责它是否需要故障隔离它的数据和规则是否边界清晰。如果两个模块总是一起修改、一起发布、一起扩容拆成两个服务可能只是增加远程调用。3. 领域事件和状态机订单状态通常不应该允许任意修改PENDING - RESERVED - PAID - COMPLETED PENDING - CANCELLED RESERVED - CANCELLED服务可以通过状态机限制合法转换并通过领域事件传播状态变化。状态机让补偿、重试和异常恢复更容易设计。4. 可观测性是微服务的基础设施至少需要指标请求量、错误率、延迟、资源使用日志结构化、带 traceId追踪跨服务调用链健康检查存活和就绪状态告警超时、错误、积压和资源异常审计关键状态和权限操作。没有可观测性服务拆分后只能看到“某个接口失败”却无法知道是哪个下游、哪条消息或哪个版本造成的。5. 依赖治理可以把依赖分级核心依赖失败时请求不能完成 重要依赖失败时可以降级部分功能 弱依赖可以异步处理 可选依赖可以直接关闭根据分级配置不同的超时、线程池、熔断和降级策略。不要让非核心服务占满核心业务的线程池和连接池。6. 数据一致性与业务补偿最终一致性需要有可执行的补偿流程订单已创建 - 库存预占失败 - 订单标记失败 - 释放已预占资源 - 发布失败事件 - 重试或转人工补偿不是简单地再调用一次接口。需要记录状态、重试次数、最后错误、下次重试时间和人工处理入口。7. API 版本和兼容策略服务升级时旧调用方可能还没有同步发布。可以采用新增字段而不是删除字段保持旧字段一段时间对接口进行版本化对事件采用向后兼容格式先升级消费者再升级生产者通过契约测试验证兼容性。API 兼容不仅包括 JSON 字段还包括状态码、错误码、分页规则、时间格式和幂等行为。8. 微服务拆分的收益评估每次拆分前后都应该评估发布耗时是否降低 故障影响范围是否缩小 扩容是否更精准 团队交付是否更独立 调用延迟是否增加 运维成本是否上升 数据一致性问题是否增加 问题定位是否更容易如果只统计服务数量没有统计交付速度、故障恢复时间和业务稳定性就很难判断拆分是否真的成功。9. 微服务治理的最小清单一个服务上线前至少应具备明确的业务边界和数据所有权 稳定的 API 契约 超时和重试策略 幂等处理 健康检查 结构化日志和 traceId 核心指标和告警 权限认证 灰度和回滚能力 数据库迁移方案 消息重试和积压处理 故障降级和补偿流程这些能力比注册中心、网关数量和服务数量更能决定微服务是否可维护。结论从单体应用到微服务不是一次代码搬家而是业务边界、数据所有权、通信方式、事务模型和运维能力的整体变化。单体应用适合早期快速交付微服务适合在业务边界稳定、团队和流量规模达到一定程度后按实际收益逐步引入。本文的重点可以归纳为微服务拆分的目标是独立演进、扩展和故障隔离服务边界应优先依据业务能力和数据所有权设计模块化单体是低风险的过渡形态每个业务数据应有明确的权威拥有者同步调用适合实时结果异步事件适合解耦和最终一致跨服务操作不能直接依赖本地数据库事务幂等、超时、重试、熔断和补偿是微服务的基础能力共享数据库会削弱服务边界应设置迁移计划可观测性和发布回滚能力必须与服务拆分同步建设服务不是越多越好拆分收益必须大于分布式复杂度。下一篇可以继续学习 Spring Cloud 微服务架构进一步了解注册中心、配置中心、网关和服务间调用如何落地。