
半年前我接手了一个帮某中型连锁零售品牌重构后端系统的活儿。他们的老系统是个典型的“大泥球”单体应用Java 8 时代写的代码量堆到了 120 万行。业务涨得快但系统慢得像蜗牛双十一这种高峰期直接宕机了三次。老板急了让我帮忙规划怎么把这套祖传代码拆成微服务还不能影响业务运行。说白了就是要在不停车的情况下把发动机换掉。说实话当时团队里有人建议直接推倒重来写一套全新的微服务代码库。我反对了。120 万行代码里藏着无数的业务逻辑和边界条件重写等于自杀。我们定下的基调是“绞杀者模式”Strangler Pattern像藤蔓一样慢慢包裹、替换原来的功能模块最后让单体自然死亡。这个过程持续了 8 个月踩了无数坑但系统确实平滑过渡了。第一阶段切分边界与 API 网关最难的不是拆代码而是决定“在哪拆”。起初我想按表结构拆发现根本行不通。因为单体的事务是跨表的拆了服务就得处理分布式事务复杂度爆炸。后来试了一圈发现还是得按 DDD领域驱动设计的业务模块切。我把系统分成了用户、订单、库存、支付四个核心域外加三个支撑域。这里有个关键的实战决策我们选 API 网关而不是直接暴露服务接口。当时团队里有争议方案 A 是前端直接调各个微服务的 HTTP 接口方案 B 是引入 Kong 做统一网关。我选了 B。为什么因为老系统有很多遗留的鉴权逻辑写在了 Controller 层直接拆出去会导致每个微服务都要重复实现一遍鉴权维护成本太高。用 Kong 统一拦截把鉴权逻辑剥离到网关层下面的服务只管业务逻辑干净多了。yamlkong.conf 核心配置片段nginx_http_conf {proxy_read_timeout 30s;,log_format main $remote_addr - $request $status;}部署网关这一步本身不复杂但有个大坑。我最初以为配置好上游服务地址就能跑了结果测试环境一跑微服务 A 调用微服务 B 居然超时了。排查了半天发现是 Docker 容器网络模式下服务发现机制没跟上。Kong 拿到的是静态 IP而微服务是动态发布的。最后我们引入了 Nacos 作为注册中心让 Kong 动态订阅服务实例问题才解决。那个晚上我在机房盯监控日志到凌晨两点头发都白了几根。第二阶段数据拆分与同步架构拆了数据还得跟着拆。单体的时候大家共用一个 MySQL 库拆服务后物理库必须分开否则微服务就没了隔离性。我采用的是“共享库-分表-独立库”的渐进策略。先不动数据只改代码里的数据访问层把 SQL 语句按领域划分。然后再做逻辑上的分离最后才是物理库的迁移。这里有个自我质疑的点。当时我觉得双写一个请求同时写单体库和新微服务库是最安全的能保证数据一致性。结果上线第一天就翻了车。原来两个写操作的耗时不同。单体库是主从架构写入稍微慢一点微服务库是新装的速度快。一旦发生网络抖动就会出现“单体库写了微服务库没写”或者反过来。更糟的是重试机制会触发重复写入导致订单号重复。坑死了。后来我放弃了双写改用 Canal 监听单体库的 Binlog 变更单向同步到微服务库。虽然实时性稍微差那么几百毫秒但对于非交易类的查询数据完全够用。对于交易类数据我们保留了单点写入只在新旧系统并行运行期间通过事务消息保证最终一致性。java// 使用 Spring Cloud Stream RabbitMQ 实现延迟消息补偿Beanpublic Binding output(MessageChannel channel) {return new Binding(orders, channel);}有意思的是在迁移库存模块时我们发现老系统里有个隐藏极深的 Bug高并发下库存扣减没有加锁靠的是数据库行锁和重试。拆成微服务后我们引入了 Redis 预扣减。这步其实不是架构要求的而是为了性能。原来单体扛 5k QPS 都抖拆出去加上 Redis 后轻松到了 15k QPS。这是这次重构带来的额外红利。收尾与反思8 个月后单体应用彻底下线120 万行代码被拆成了 14 个微服务每个服务代码量控制在 10 万行以内。部署时间从原来的 40 分钟缩短到了 5 分钟因为是容器化部署CI/CD 流水线一天能发十几个版本。但我并不认为这就是完美的终局。微服务带来了分布式调用的复杂度网络抖动、服务降级、熔断限流这些问题在单体时代是不存在的。我们为此写了大量的测试用例甚至专门建了一个混沌工程实验室定期故意杀容器看系统能不能自愈。如果你也在做类似的迁移记住一点不要追求一步到位。先把最痛的那个模块拆出去让团队体验一下微服务的好处比如独立部署、快速迭代其他人才会有信心跟进。架构演进不是数学证明题没有标准答案只有最适合当下团队能力和业务节奏的方案。本文基于实际项目经验整理欢迎在评论区交流技术问题。