当前位置: 首页 > news >正文

单体到微服务拆分方案

在当今微服务架构已成为主流技术范式的时代,从单体应用向微服务的系统化迁移是许多企业提升系统扩展性、灵活性与可维护性的必然选择。然而,这一转型过程绝非简单的代码拆分,而是一项涉及技术、架构及组织协调的系统工程。若缺乏清晰的规划与可靠的执行策略,不仅难以收获微服务带来的收益,还可能引入新的复杂性,导致项目面临更高风险。

一个成功的迁移方案必须是完整、可操作且平滑的。它需要一套明确的阶段规划——从现状评估、边界划分、依赖解耦,到数据迁移、流量切换和治理体系构建——每个阶段都应配备相应的技术工具与保障机制。尤为关键的是,必须着力解决服务拆分后带来的数据耦合问题,即传统单体数据库中跨模块的紧密关联(如JOIN操作、共享事务)被拆散后,如何保证数据一致性、解决跨服务查询以及实现有效的分布式事务管理。

因此,一个经过验证的渐进式策略,辅以清晰的演进路线和配套治理手段,是确保系统平滑迁移、持续稳定运行的基石。

一、总体迁移策略:渐进式迁移

微服务改造是一场外科手术,而不是推倒重建。必须采用渐进式策略,以绞杀者模式(Strangler Pattern)为核心指导思想,逐步替换单体应用的功能,保证业务持续可用。

迁移流程概览

准备阶段
评估与规划
服务拆分与迁移
绞杀者模式
数据库迁移
四阶段双写方案
解决数据耦合
四种核心策略
治理与优化
建立监控与治理体系

接下来,我们详细拆解图中的每一个环节。

二、准备阶段:评估与规划

  1. 深度剖析现有单体:绘制系统架构图、模块依赖图、数据库ER图。识别出高内聚、低耦合的模块边界(例如用户、订单、商品等),这些将是首批拆分的候选者。

  2. 制定拆分原则:
    ◦ 单一职责:每个服务只负责一个明确的业务能力。

    ◦ 围绕业务边界:基于领域驱动设计(DDD) 的限界上下文(Bounded Context)来划分服务,而不是根据技术层次。

    ◦ 团队结构匹配:服务划分应尽量与团队结构匹配(参考康威定律)。

  3. 技术选型与基建先行:在拆分代码之前,先搭建好微服务的基础设施,这是平滑迁移的保障。
    ◦ API网关:Spring Cloud Gateway, Kong

    ◦ 服务注册与发现:Nacos, Eureka, Consul

    ◦ 配置中心:Nacos, Apollo

    ◦ 监控追踪:Prometheus + Grafana, SkyWalking, ELK

三、服务拆分与迁移:绞杀者模式

这是迁移的核心战术,通过在单体外部逐步构建新服务,最终“绞杀”掉单体。

  1. 部署API网关:将所有外部流量导向网关。网关成为系统的唯一入口,这是流量切换的控制点。

  2. 识别并迁移首个服务:选择一个相对独立、边界清晰、非核心的模块(如“用户服务”)作为起点。

  3. 实现并行运行:
    ◦ 在网关配置路由规则,将指向新功能的请求(如 /api/users/**)路由到新的用户微服务。

    ◦ 其他所有请求(如 /api/orders/**)仍然路由到原单体应用。

    ◦ 此时,用户相关的增删改查逻辑在微服务中,而订单逻辑仍在单体中,两者可能仍需通过数据库或内部调用关联。

// Spring Cloud Gateway 路由配置示例
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {return builder.routes().route("user_service", r -> r.path("/api/users/**").uri("lb://user-service")) // 导向新用户服务.route("monolith_app", r -> r.path("/api/**").uri("lb://monolith-app")) // 其他请求仍导向单体.build();
}
  1. 重复迭代:重复步骤2和3,逐步将订单、商品、支付等模块一个个地迁移出去,像藤蔓一样逐渐包裹并最终取代单体。

四、数据库迁移:最艰难的部分

这是拆分过程中风险最高的环节。必须保证数据在迁移过程中不丢失、不错乱。推荐 “四阶段双写”在线迁移方案。

阶段目标读写策略关键操作
第一阶段:全量同步将历史基础数据复制到新库只读旧库,只写旧库使用数据同步工具(如Canal, Debezium)将历史数据全量复制到新数据库。
第二阶段:增量同步与双写保持新旧库数据实时同步读旧库,写旧库+写新库开启双写:所有写操作(增删改)同时写入旧库和新库。同时,继续通过日志工具同步增量数据,并定期校验数据一致性。
第三阶段:读流量灰度验证新库数据的正确性部分读新库,部分读旧库;双写在网关或服务层做灰度发布,将一小部分读流量(如1%)导向新库,大部分读请求仍走旧库。密切监控,如有问题,迅速切回。
第四阶段:完全切换完成迁移只读新库,只写新库逐步将100%的读流量切换到新库。稳定运行一段时间后,停止写入旧库,并下线旧库相关代码。旧库暂时别删,以备回滚。

双写逻辑的伪代码示例

@Service
@Transactional
public class OrderService {@Autowiredprivate OrderRepository orderRepository; // 旧库DAO@Autowiredprivate NewOrderRepository newOrderRepository; // 新库DAOpublic Order createOrder(Order order) {// 1. 写入旧库(主事务)Order savedOrder = orderRepository.save(order);// 2. 异步写入新库 (防止新库写入失败影响主流程)try {newOrderRepository.saveAsync(savedOrder);} catch (Exception e) {// 记录日志并告警,启动补偿任务,最终确保数据一致log.error("Async write to new DB failed, orderId: {}", savedOrder.getId(), e);}return savedOrder;}
}

五、解决服务拆分后的数据耦合问题

这是你最关心的问题。一旦服务拆分,数据库随之拆分,传统的JOIN查询就失效了。以下是四种主流的解决方案,各有适用场景。

策略原理适用场景优点缺点
API 组合由API组合器(如网关或独立服务)调用多个服务的API,在内存中聚合数据。 实时性要求高、关联服务少、数据量小的查询。实现简单,无需数据冗余,数据最实时。多次网络调用,延迟高,稳定性差(依赖所有服务都健康)。
数据冗余将其他服务的常用数据冗余存储到本地。例如,订单服务冗余商品名称和价格。读多写少,对数据强一致性要求不高的场景。查询性能极佳(无网络调用),服务自治。数据一致性难保障,需通过事件驱动同步更新,架构更复杂。
事件驱动同步服务发布领域事件(如UserUpdatedEvent),其他服务订阅并更新本地私有视图。构建复杂查询、报表、搜索平台。彻底解耦,查询性能极高,适合大数据量。数据有延迟(最终一致性),架构复杂,技术门槛高。
CQRS模式命令与查询责任分离。写模型保障事务,读模型为查询量身定制,通过事件同步数据。读写负载差异极大的复杂业务场景。读写端可独立优化,扩展性极佳。架构非常复杂,学习成本高,需要维护两套模型。

策略选择建议:

• 简单查询、实时性要求高 → API 组合

• 常用查询、追求性能 → 数据冗余 + 事件同步

• 复杂查询、报表、搜索引擎 → 事件驱动同步到专用读库

• 超高并发、复杂业务 → CQRS

数据冗余与事件驱动模式(最常用)

订单服务需要显示商品信息为例:

  1. 数据冗余:在订单表orders中,除了product_id,还冗余存储product_name和product_price(下单时的快照)。

  2. 事件驱动同步:
    ◦ 当商品服务的商品名称或价格更新时,它会发布一个ProductUpdatedEvent事件到消息队列(如Kafka)。

    ◦ 订单服务订阅该事件。一旦收到,它就更新本地所有包含该商品的订单记录中的冗余信息。

// 商品服务:发布事件
@Service
public class ProductService {@Autowiredprivate ApplicationEventPublisher eventPublisher;public void updateProduct(Product product) {// ... 更新逻辑// 发布事件eventPublisher.publishEvent(new ProductUpdatedEvent(product.getId(), product.getName(), product.getPrice()));}
}// 订单服务:订阅事件并更新本地数据
@Service
public class OrderEventListener {@EventListenerpublic void handleProductUpdatedEvent(ProductUpdatedEvent event) {// 根据事件中的商品ID,找到所有相关的订单List<Order> orders = orderRepository.findByProductId(event.getProductId());for (Order order : orders) {// 更新订单中冗余的商品信息order.setProductName(event.getNewName());order.setProductPrice(event.getNewPrice());orderRepository.save(order);}}
}

这种方式实现了服务间的解耦,订单服务无需直接调用商品服务的API,而是通过事件异步通信。

六、治理与优化

  1. 分布式事务与数据一致性:对于必须保证强一致性的场景,采用Saga模式(通过补偿操作回滚)或可靠事件队列(保证事件至少投递一次,消费者需幂等处理)。
  2. 建立完善的监控:微服务拆分会增加故障点,必须建立强大的监控体系(日志、链路追踪、指标监控),以便快速定位问题。
  3. 团队与流程变革:微服务需要DevOps文化和小团队自治。每个团队应对其负责的服务的全生命周期负责。

总结

单体到微服务的平滑迁移是一个系统工程,其核心路线可以概括为:

技术层面:基础设施先行 → 采用绞杀者模式渐进拆分 → 实施双写方案迁移数据库 → 根据场景选择数据聚合策略。
组织层面:评估规划 → 小步快跑 → 建立治理 → 持续优化。

从传统的基于数据库的集中式数据建模,转向基于服务的分布式数据建模数据冗余事件驱动是解决跨服务查询最主要和最有效的手段。

迁移过程漫长且复杂,切记不要追求一步到位。每完成一个小步骤,都要进行充分测试和验证,确保稳定后再继续下一步。

问题

一、为何选择代码侵入式双写而非Canal等零侵入方案?

这是一个关于 “控制力” 与 “复杂度” 的权衡。两种方案对比如下:

特性应用层双写 (代码侵入)Canal (基于Binlog)
原理在业务代码中显式地同时写入新旧两个数据库。通过监听数据库Binlog日志,解析后异步同步到新库。
代码侵入性高,需修改原有代码。极低(近乎零侵入),对老系统无感知。
业务逻辑处理强。可在写入前进行任何复杂的业务逻辑计算、数据转换、校验和封装。弱。通常只能获取到数据库字段变更,难以处理复杂业务逻辑(如:需要调用RPC接口获取关联数据)。
数据一致性易于保证强一致性(通过本地事务)。最终一致性,有延迟风险。
实时性高,几乎是实时的。较高,但有延迟(通常在毫秒到秒级)。
架构复杂度低,只需在应用代码中添加写操作。高,需引入并维护Canal中间件、MQ等组件。
异构数据转换非常灵活,可以在代码中任意映射和组装数据。较弱,复杂的数据聚合和转换需要额外的客户端处理。

选择代码侵入式双写的主要原因如下:

  1. 业务逻辑的复杂性:微服务拆分不仅是数据的平移,更是业务模型和数据结构的重构。旧单体的一个宽表,可能被拆分成新微服务中的多个实体,并包含一些需要实时计算或从其他服务获取的衍生字段。Binlog日志里只有“数据”,没有“业务”。通过代码双写,你可以在写入前完成所有这些复杂的业务逻辑处理和数据组装,这是Canal难以做到的。
  2. 数据一致性的控制力:在第二阶段:增量同步与双写,核心目标是保证新旧库数据的最终一致性。代码双写允许你使用本地事务(如果新旧库是同一个数据库实例)或补偿机制(如记录日志、定时对账)来尽可能地保证一致性,你对这个过程有完全的控制力。
  3. 迁移过程的可控性:双写是一个显式的操作,你可以非常清楚地知道数据是如何流转的,并在代码中添加日志、监控和熔断开关,便于排查问题。而Canal是隐式的同步,更像一个黑盒,一旦出现问题,排查链路较长。
  4. 简化技术栈:在迁移初期,引入Canal会额外增加MQ、Canal服务器、ZK等运维复杂度。而代码双写仅依赖现有的数据库和应用,架构更简单。

总而言之,在迁移的“双写”阶段,我们优先选择代码侵入方案,是因为我们需要业务的深度融合和极强的控制力,以应对复杂的数据转换和一致性要求。 Canal更适用于迁移完成后,作为不同系统间持续的数据同步和备份机制,或者用于同步那些业务逻辑极其简单的数据。

二、为何在内层网关分流,而非外层网关?

这涉及到对网关分层架构的理解。在一个典型的微服务体系中,网关通常分为两层:

• 外层网关 (API Gateway):直面公网,是系统的总入口。负责全局流量管理、安全认证、限流熔断、日志记录等跨横切面关注点。

• 内层网关 (微服务网关 / Sidecar):通常指与服务部署在一起的服务网格边车(如Istio的Envoy),或在集群内部独立部署的网关。负责服务发现、动态路由、负载均衡、服务间认证和熔断。

选择在内层网关进行分流的原因:

  1. 职责分离:外层网关的核心职责是对外。它不应该关心也无法理解系统内部正在进行的迁移状态(哪个服务已迁移,哪个服务还是单体)。它的路由规则通常是粗粒度的(基于Path、Header等)。而迁移分流是一个系统内部的、精细化的运维动作,理应由更了解服务拓扑的内层网关来负责。
  2. 灵活性:内层网关(如Istio)可以通过VirtualService和DestinationRule等配置,实现极其灵活的路由规则。例如,你可以轻松实现:将包含 Header version: new 的请求路由到新用户服务,否则路由到旧单体。这种细粒度的控制在外层网关难以实现,且会污染其配置。
  3. 安全与稳定性:迁移过程中的流量切换是一个高风险操作。在内层网关进行,一旦出现故障,影响范围可以被控制在服务网格内部,不会直接暴露到公网入口层。同时,修改内层网关的配置通常比修改外层网关更安全,对全局影响更小。
  4. 技术选型:常见的外层网关(如Nginx)虽然也能实现一定程度的动态路由,但服务网格(如Istio)提供的动态、热生效、更强大的流量治理能力(如按百分比切流、故障注入)更适合这种迁移场景。

策略建议:
外层网关:配置基于路径前缀的粗粒度路由。例如,所有以 /api/ 开头的请求都路由到内部集群。

内层网关 (如Istio):配置细粒度的路由规则。例如,将 /api/users/** 的请求,按90%和10%的比例,分别路由到 monolith-app 和 user-service。这样,分流策略的调整完全在内部完成,无需改动外层网关。

三、如何具体实施按百分比灰度分流并不停机调整?

这可以通过配置中心和网关的动态路由能力相结合来实现。以下是基于Spring Cloud Gateway + Nacos(或其他配置中心如Apollo)的实战方案:

1. 核心原理

将路由配置(特别是权重信息)存储在配置中心(Nacos)。Spring Cloud Gateway监听这些配置,一旦变化,就动态更新其内存中的路由定义,无需重启服务。

2. 实施步骤

第一步:在Nacos中创建灰度发布配置
在Nacos中创建一个dataId为 gateway-routes.json 的配置,内容如下:

{"routes": [{"id": "user_service_route","order": 0,"predicates": [{"name": "Path","args": {"pattern": "/api/users/**"}}],"filters": [],"uri": "lb://user-service","metadata": {"version": "v1"},"weight": 10 // 10%的流量导向新服务},{"id": "monolith_user_route","order": 0,"predicates": [{"name": "Path","args": {"pattern": "/api/users/**"}}],"filters": [],"uri": "lb://monolith-app","metadata": {"version": "v0"},"weight": 90 // 90%的流量仍导向单体}]
}

第二步:在Spring Cloud Gateway中集成Nacos
在Gateway的application.yml中配置:

spring:cloud:gateway:routes: # 初始路由配置,会被Nacos覆盖discovery:locator:enabled: true # 开启从服务发现动态创建路由nacos:server-addr: ${NACOS_HOST:localhost}:${NACOS_PORT:8848}data-id: gateway-routes.jsongroup: DEFAULT_GROUPnamespace: ${NACOS_NAMESPACE:}

第三步:编写动态路由逻辑(可选)
你可以编写一个@RefreshScope的Bean,监听Nacos配置变化,并动态更新Gateway的路由定义。Spring Cloud Gateway Starter与Nacos本身已支持这种动态性。

第四步:不停机调整百分比

  1. 你需要修改灰度策略时,直接登录Nacos控制台。
  2. 找到 gateway-routes.json 配置。
  3. 直接修改 weight 的值(例如,将新服务的权重从 10 调整为 30)。
  4. 点击发布。Nacos会主动将新配置推送到Gateway。
  5. Spring Cloud Gateway接收到新配置后,自动、无缝地更新路由规则。整个过程在毫秒级完成,对正在处理的请求无感知,真正做到不停机调整。

3. 增强实践(基于Header的灰度)

除了权重,更常见的灰度方式是基于请求Header。例如,给内部测试人员的手机App提供一个特定的Header(如 x-gray-tag: test-user)。
在Nacos配置中,可以增加一个Predicate:

{"name": "Header","args": {"key": "x-gray-tag","regexp": "test-user"}
}

这样,只有带这个Header的请求才会被路由到新服务,实现更精准的灰度测试。

4. 监控与反馈

• 在网关层面集成Prometheus和Grafana,监控新老服务的QPS、延迟、错误率。

• 设置报警规则,一旦新服务的错误率飙升,立即在Nacos上将权重调回0%,实现快速回滚。

通过这套基于配置中心的方案,你就能轻松、安全、动态地掌控整个灰度发布过程。


文章转载自:

http://niIqiz8b.fhwfk.cn
http://QaL9usrW.fhwfk.cn
http://QRz2qeoE.fhwfk.cn
http://TTWLXFy4.fhwfk.cn
http://ntqS8oVw.fhwfk.cn
http://dSEKYvdK.fhwfk.cn
http://bZUGbp0p.fhwfk.cn
http://PFnYgwQ1.fhwfk.cn
http://ug5PYaTx.fhwfk.cn
http://p3pj37ao.fhwfk.cn
http://bLfGgAty.fhwfk.cn
http://7yWuwhVx.fhwfk.cn
http://WqWLrbep.fhwfk.cn
http://8w8CbRt3.fhwfk.cn
http://3Tu4FUWf.fhwfk.cn
http://MZ6yZ4ME.fhwfk.cn
http://lZJriKlo.fhwfk.cn
http://5OCe1Z88.fhwfk.cn
http://6PUUJFNC.fhwfk.cn
http://TzDMMQdM.fhwfk.cn
http://WnwcdyXz.fhwfk.cn
http://SzSJT32q.fhwfk.cn
http://mWkjq5pe.fhwfk.cn
http://Y8kWqgsd.fhwfk.cn
http://J26oaM6X.fhwfk.cn
http://7fkfteIF.fhwfk.cn
http://1gDr7g8L.fhwfk.cn
http://9zXYx05p.fhwfk.cn
http://NnNKmZ4b.fhwfk.cn
http://UukZ6MQB.fhwfk.cn
http://www.dtcms.com/a/381646.html

相关文章:

  • 云端服务器使用指南:如何跨机传输较大文件(通过windows自带工具远程桌面连接 非常方便)
  • Linux 高性能 I/O 事件通知机制的核心系统调用—— `epoll_ctl`
  • 域格YM310 X09移芯CAT1模组HTTPS连接服务器
  • 连续随机变量无法用点概率描述出现了概率密度函数(Probability Density Function, PDF)
  • Go语言实战案例 — 工具开发篇:Go 实现条形码识别器
  • 洛谷-P1923 【深基9.例4】求第 k 小的数-普及-
  • DeerFlow实践:华为ITR流程的评审智能体设计
  • K均值聚类(K-Means)算法介绍及示例
  • 【企业架构】TOGAF-4A架构概览
  • 华为防火墙三层部署模式
  • Linux Kernel Core API:printk
  • 空间信息与数字技术专业主要学什么技能?
  • 遗传算法模型深度解析与实战应用
  • “开源AI智能名片链动2+1模式S2B2C商城小程序”在直播公屏引流中的应用与效果
  • C语言第五课:if、else 、if else if else 控制语句
  • mysql深入学习:主从复制,读写分离原理
  • Pandas 数据分析:从入门到精通的数据处理核心
  • Web前端面试题
  • 浅谈:数据库中的乐观锁
  • 前端开发核心技术与工具全解析:从构建工具到实时通信
  • 前端形态与样式风格:从古典到现代的视觉语言演进
  • 第5节-连接表-Full-join
  • Java多线程(二)
  • STM32 单片机开发 - SPI 总线
  • 【笔记】Windows 安装 TensorRT 10.13.3.9(适配 CUDA 13.0,附跨版本 CUDA 调用维护方案)
  • 基于PHP的鲜花网站设计与实现
  • 如果系统里没有cmake怎么办? 使用pip install来安装cmake
  • QRCode React 完全指南:现代化二维码生成解决方案
  • 关于电脑连接不到5g的WiFi时的一些解决办法
  • Cursor中文界面设置教程