我们团队最近做了一次大的微服务拆分和代码重构,把一个维护了五年的"大泥球"单体应用,拆分成了多个微服务,并且对代码进行了全面重构。

这个过程,持续了将近半年,非常痛苦,踩了很多坑,也有很多争论和分歧。但是结果也很令人欣慰:代码质量大幅提升,开发效率明显提高,系统稳定性也更好了。以前改一个功能要小心翼翼,生怕改出问题,现在改代码心里有底多了。

今天,我想分享我们这次微服务拆分和代码重构的经历,包括为什么要拆分、怎么拆分、拆分过程中遇到的问题、代码重构的原则和实践、以及我们的经验和教训。

一、为什么要拆分和重构

先说说我们为什么要做微服务拆分和代码重构。

我们的核心业务系统,是一个五年前开始做的电商平台。最开始的时候,团队只有三个人,为了快速上线,就用了单体架构,所有功能都在一个应用里。那时候代码量不大,功能也不复杂,单体架构没什么问题,开发效率还很高。

但是,随着业务的发展,功能越来越多,代码量越来越大,团队也从三个人扩展到了二十多个人。这时候,单体架构的问题就越来越明显了。

问题一:代码混乱,维护困难

五年的时间,不同的人写了不同风格的代码,没有统一的规范。有的模块写得还不错,有的模块就是"能跑就行"。代码里充满了各种"祖传代码",没人敢改,因为改了不知道会出什么问题。

业务逻辑到处都是,Controller里有业务逻辑,Service里有数据库操作,DAO里有业务判断,甚至JSP页面里都有Java代码。代码耦合严重,改一个地方,可能影响很多地方。

问题二:编译部署慢,开发效率低

代码量大了之后,编译一次要十几分钟,启动一次要几分钟。开发人员改一行代码,要等很久才能看到效果,非常影响开发效率。

部署也是个问题。每次发布,都要全量部署,发布一次要半个小时,而且风险很大,一个小功能的改动,可能导致整个系统出问题。所以我们不敢频繁发布,只能攒一堆功能一起发布,发布的时候如临大敌,经常要加班到深夜。

问题三:团队协作困难

二十多个人在同一个代码库里开发,经常出现代码冲突。你改的代码和我改的代码冲突了,合并代码要花很多时间。而且,一个人的代码出了问题,可能影响所有人,导致大家都没法工作。

代码的所有权也不清晰。这个模块是谁负责的?出了问题找谁?经常是互相推诿,或者谁都不敢改,最后只能找最老的员工来改。

问题四:技术栈老旧,无法升级

这个系统用的是五年前的技术栈,Spring 3.x,Struts 2,JSP,MySQL 5.5。这些技术,有的已经停止维护了,有的有安全漏洞,但是我们不敢升级,因为代码耦合太严重,升级一个框架,可能导致整个系统崩溃。

我们想用新的技术,比如Spring Boot、微服务、Docker等,但是在这个老系统里根本用不了。技术债越积越多,团队的技术能力也在退步,新人来了都不想碰这个老系统。

问题五:扩展性差,性能瓶颈

单体应用,只能整体扩容。比如,订单模块压力大了,只能把整个应用多部署几个实例,但是其他模块(比如用户模块、商品模块)其实不需要那么多实例,造成了资源浪费。

而且,有些模块的性能瓶颈,会影响整个系统。比如,报表模块的慢查询,会把数据库连接池占满,导致其他模块也连不上数据库,整个系统卡死。

因为这些问题,我们团队的开发效率越来越低,加班越来越多,大家都很痛苦。技术负责人也意识到,再不做改变,这个系统就要废掉了。于是,我们决定做微服务拆分和代码重构。

二、怎么拆分:我们的策略

决定做微服务拆分之后,我们面临的第一个问题就是:怎么拆?

微服务拆分,说起来容易,做起来难。拆得太细,服务太多,运维复杂;拆得太粗,和单体没什么区别。而且,拆分的过程中,业务不能停,系统还要正常运行,这就更难了。

我们经过反复讨论,制定了以下拆分策略。

策略一:按业务领域拆分,而不是按技术层拆分

微服务拆分,最常见的错误就是按技术层拆分,比如把Controller层拆成一个服务,Service层拆成一个服务,DAO层拆成一个服务。这样拆分是错误的,因为微服务应该是高内聚、低耦合的,按业务领域拆分,每个服务负责一个完整的业务领域,而不是一个技术层。

我们按照领域驱动设计(DDD)的思路,先梳理了系统的业务领域,划分为以下几个核心领域:

  • 用户领域:用户注册、登录、个人信息、权限等
  • 商品领域:商品管理、分类、库存、价格等
  • 订单领域:下单、支付、发货、退款等
  • 营销领域:优惠券、活动、促销等
  • 内容领域:文章、评论、广告等
  • 报表领域:数据统计、报表分析等

每个领域,拆分成一个微服务。这样,每个服务的职责清晰,高内聚,低耦合。

策略二:渐进式拆分,而不是大爆炸式重写

微服务拆分,另一个常见的错误就是大爆炸式重写:停掉所有新功能,花几个月时间,把整个系统用微服务重写一遍,然后一次性上线。这样做风险很大,因为重写的过程中,业务需求还在变化,等你重写完,需求已经变了,而且重写的系统没有经过生产验证,上线很容易出问题。

我们采用的是渐进式拆分的策略:

  1. 先从边缘的、简单的模块开始拆,比如内容管理、报表等,这些模块对核心业务影响小,风险低
  2. 拆一个,上线一个,验证一个,确保稳定之后再拆下一个
  3. 核心模块(用户、商品、订单)最后拆,因为这些模块最复杂,风险最大,等我们有了足够的微服务经验之后再拆
  4. 拆分的过程中,新功能尽量在新的微服务里开发,老系统只做必要的维护和bug修复

这样,整个拆分过程是渐进的,风险可控,业务也不会停。

策略三:绞杀者模式,逐步替换老系统

在渐进式拆分的过程中,我们用了"绞杀者模式"(Strangler Pattern)。

绞杀者模式的思路是:在老系统外面,包一层新的微服务,新的功能和请求,都走新的微服务,老系统只处理还没有迁移的功能。随着时间的推移,新的微服务越来越多,老系统处理的功能越来越少,最后老系统被"绞杀"掉,完全被新的微服务替代。

具体做法是:

  1. 部署一个API网关,所有请求都先经过API网关
  2. API网关根据请求的路径,把已经拆分的功能转发到新的微服务,还没有拆分的功能转发到老系统
  3. 每拆分一个模块,就修改API网关的路由,把这个模块的请求转发到新的微服务
  4. 等所有模块都拆分完了,老系统就可以下线了

这样,用户完全感觉不到系统在拆分,体验是一致的,风险也很低。

策略四:数据拆分,每个服务独立数据库

微服务拆分,不仅要拆代码,还要拆数据。每个微服务,应该有自己独立的数据库,服务之间不能直接访问对方的数据库,只能通过API调用。

数据拆分比代码拆分更难,因为数据之间有各种关联和依赖。我们的做法是:

  1. 先梳理数据的归属,哪些表属于哪个服务
  2. 对于跨服务的关联,去掉外键,通过服务调用来获取数据
  3. 对于跨服务的事务,用最终一致性代替强一致性,用消息队列来实现
  4. 数据迁移的时候,写双写,新老数据库同时写,验证数据一致之后,再切到新数据库

数据拆分是微服务拆分中最难的部分,一定要谨慎,做好验证和回滚方案。

三、拆分过程中遇到的问题

拆分的过程中,我们遇到了很多问题,下面说说几个最典型的。

问题一:分布式事务

单体应用的时候,所有操作都在一个数据库事务里,要么都成功,要么都失败,很简单。拆成微服务之后,一个业务操作可能涉及多个服务,比如下单,要调用订单服务、库存服务、支付服务,每个服务都有自己的数据库,这时候就涉及分布式事务的问题。

分布式事务,没有完美的解决方案。我们尝试过以下几种方案:

  1. 两阶段提交(2PC):用XA事务,但是性能差,耦合高,而且不是所有数据库和框架都支持,我们放弃了
  2. TCC(Try-Confirm-Cancel):每个服务提供Try、Confirm、Cancel三个接口,由事务协调器来调用。这个方案性能不错,但是开发成本高,每个服务都要写三个接口,而且要处理各种异常情况
  3. 最终一致性 + 消息队列:用消息队列来实现最终一致性。比如下单的时候,订单服务创建订单,然后发消息到消息队列,库存服务和支付服务消费消息,做自己的操作。如果失败了,就重试,或者人工补偿。这个方案性能好,耦合低,但是一致性是最终的,不是实时的,而且要处理消息重复、消息丢失等问题

最后,我们大部分场景用的是"最终一致性 + 消息队列"的方案,只有少数对一致性要求特别高的场景,用了TCC。

分布式事务,是微服务拆分中最头疼的问题之一,一定要根据业务场景,选择合适的方案,不要追求强一致性,大部分场景最终一致性就够了。

问题二:服务间调用复杂,链路追踪难

拆成微服务之后,一个请求可能要经过好几个服务,出了问题,很难定位是哪个服务出了问题。而且,服务之间的调用关系复杂,谁调用了谁,调用了多少次,耗时多少,都不清楚。

为了解决这个问题,我们做了以下几件事:

  1. 引入分布式追踪系统:用Jaeger做分布式追踪,每个请求都有一个Trace ID,贯穿所有服务,出了问题可以根据Trace ID查看完整的调用链
  2. 统一日志规范:所有服务的日志格式统一,都包含Trace ID,日志集中收集到ELK,出了问题可以根据Trace ID搜索所有相关日志
  3. 服务监控告警:每个服务都做监控,包括响应时间、错误率、吞吐量等,超过阈值自动告警
  4. API网关统一入口:所有请求都经过API网关,在网关层做统一的鉴权、限流、日志、监控

有了这些,排查问题的效率提升了很多。

问题三:服务太多,运维复杂

拆成微服务之后,服务数量从1个变成了十几个,每个服务都要部署、监控、扩容、升级,运维复杂度大幅提升。

为了解决这个问题,我们做了以下几件事:

  1. 容器化部署:所有服务都用Docker容器部署,环境一致,部署简单
  2. 容器编排:用Kubernetes做容器编排,自动部署、自动扩容、自动恢复
  3. CI/CD流水线:用Jenkins做持续集成和持续部署,代码提交后自动构建、自动测试、自动部署,发布效率大幅提升
  4. 配置中心:用Apollo做配置中心,所有服务的配置统一管理,动态更新,不需要重启服务
  5. 服务注册发现:用Consul做服务注册发现,服务之间通过服务名调用,不需要硬编码IP地址

有了这些基础设施,运维效率提升了很多,虽然服务多了,但是运维并没有想象中那么累。

问题四:团队技能不足,学习成本高

微服务、Docker、Kubernetes、DevOps,这些都是新技术,我们团队之前都没怎么接触过,学习成本很高。

为了解决这个问题,我们做了以下几件事:

  1. 培训和分享:定期组织技术培训和分享,让大家学习微服务相关的技术
  2. 结对编程:有经验的人和没经验的人结对编程,一起做拆分,在实践中学习
  3. 制定规范和模板:制定微服务开发规范,提供项目模板,新服务可以基于模板快速创建,减少重复工作
  4. 先试点再推广:先让几个技术骨干做试点,踩坑积累经验,然后再推广到整个团队

虽然学习成本高,但是大家的技术能力都提升了,团队的整体技术水平也上了一个台阶。

四、代码重构:从烂代码到优雅代码

微服务拆分的过程中,我们也对代码进行了全面重构。因为老代码实在太烂了,如果只是把代码从一个仓库搬到另一个仓库,不做重构,那拆分就没有意义。

下面说说我们代码重构的原则和实践。

原则一:先理解,再重构

重构之前,一定要先理解代码的业务逻辑。老代码虽然烂,但是它是经过五年生产验证的,里面隐含了很多业务规则和边界条件。如果不理解就重构,很容易改出问题。

我们的做法是:

  1. 先读代码,理解业务逻辑,画流程图和时序图
  2. 和产品经理、老员工确认,确保理解正确
  3. 写测试用例,覆盖主要的业务场景和边界条件
  4. 在有测试保护的情况下,再进行重构

测试是重构的安全网。没有测试,就不要重构。

原则二:小步重构,持续集成

重构不要一步到位,要小步进行。每次只做一个小的重构,比如提取一个方法,重命名一个变量,提取一个类,然后运行测试,确保没问题,再提交代码。

小步重构的好处是:

  • 风险低,每次改动小,出了问题容易回滚
  • 容易review,代码review的时候,小的改动更容易发现问题
  • 持续集成,每次重构都合并到主干,不会出现长期的分支,避免合并地狱

我们的重构,就是在日常开发中持续进行的,不是专门花几个月时间来重构。每次做需求的时候,顺手把相关的烂代码重构一下,时间长了,代码质量就慢慢提升了。

原则三:分层清晰,职责单一

老代码最大的问题就是分层不清,职责混乱。我们重构的时候,严格按照分层架构来组织代码:

  • Controller层:只负责接收请求,参数校验,调用Service,返回结果,不包含业务逻辑
  • Service层:负责业务逻辑,事务管理,调用DAO和其他服务,不处理HTTP相关的东西
  • DAO层:只负责数据库操作,不包含业务逻辑
  • Domain层:领域模型,包含实体、值对象、领域服务等,封装核心业务逻辑
  • 基础设施层:技术框架、第三方服务、消息队列、缓存等

每一层的职责清晰,只做自己该做的事情。这样,代码结构清晰,可读性好,维护起来也容易。

原则四:命名规范,注释适当

老代码另一个大问题就是命名混乱,注释缺失或者过时。比如变量名叫abdatatemp,方法名叫doIthandleprocess,根本不知道是什么意思。

我们重构的时候,非常重视命名:

  • 类名、方法名、变量名,都要有意义,能准确表达其用途
  • 命名风格统一,比如类名用大驼峰,方法名和变量名用小驼峰,常量用全大写
  • 布尔变量用ishascan开头,比如isValidhasPermission
  • 方法名用动词开头,比如createOrdergetUserByIdcalculatePrice

注释方面,我们的原则是:

  • 代码能表达的,就不要写注释,好的代码是自解释的
  • 注释只写代码表达不了的东西,比如业务背景、设计思路、注意事项、TODO等
  • 注释要和代码保持一致,代码改了,注释也要改,过时的注释比没有注释更糟糕

原则五:消除重复,抽象复用

老代码里有大量的重复代码,同样的逻辑,在不同的地方复制粘贴,改的时候要改好几个地方,很容易漏改。

我们重构的时候,非常重视消除重复:

  • 相同的逻辑,提取成公共方法或者公共类
  • 相同的业务流程,提取成模板方法或者策略模式
  • 相同的工具方法,放到工具类里
  • 相同的配置,提取成配置项或者常量

消除重复,不仅减少了代码量,也降低了维护成本。但是也要注意,不要过度抽象,为了复用而复用,反而增加了代码的复杂度。

原则六:错误处理统一,异常体系清晰

老代码的错误处理非常混乱,有的地方吞掉异常,有的地方返回null,有的地方返回错误码,有的地方抛异常,非常不统一。

我们重构的时候,建立了统一的异常体系:

  • 自定义业务异常BusinessException,包含错误码和错误信息
  • 系统异常用RuntimeException,不需要捕获,由全局异常处理器统一处理
  • Controller层用全局异常处理器,捕获所有异常,统一返回错误响应
  • 错误码统一管理,每个业务模块有自己的错误码段
  • 异常信息要清晰,包含足够的上下文,方便排查问题

这样,错误处理统一了,代码也简洁了很多。

五、经验和教训

最后,总结一下我们这次微服务拆分和代码重构的经验和教训。

经验一:微服务不是银弹,不要为了微服务而微服务

微服务有很多好处,但是也有很多代价。分布式事务、服务调用、运维复杂度、团队技能要求,这些都是微服务的代价。如果你的团队规模小,业务不复杂,单体架构可能更适合你。

不要因为微服务火,就盲目上微服务。先评估自己的业务和团队,确认确实需要微服务,再上。而且,微服务也不是一步到位的,可以先从模块化单体开始,等模块边界清晰了,再拆成微服务。

经验二:拆分之前,先做好模块化

微服务拆分的前提,是代码有清晰的模块边界。如果你的代码是"大泥球",模块之间耦合严重,直接拆成微服务会非常痛苦,而且拆出来的服务也会是"分布式大泥球"。

所以,拆分之前,先在单体应用里做好模块化,理清模块边界,减少模块之间的耦合。等模块边界清晰了,再拆成微服务,就水到渠成了。

经验三:基础设施先行

微服务需要很多基础设施的支撑,比如容器化、CI/CD、服务注册发现、配置中心、API网关、分布式追踪、监控告警等。如果这些基础设施没有建好,就开始拆微服务,会非常痛苦,运维和开发效率都会很低。

所以,微服务拆分之前,先把基础设施建好。至少要有容器化部署、CI/CD、服务注册发现、监控告警这几个基础组件,然后再开始拆分。

经验四:重构要有耐心,不要追求一步到位

代码重构是一个长期的过程,不要指望花几个月时间,就能把所有烂代码都改好。而且,重构的过程中,业务需求还在变化,新代码还在写,如果不注意,新写的代码也可能变成烂代码。

所以,重构要有耐心,要在日常开发中持续进行。每次做需求的时候,顺手重构一下相关的代码,时间长了,代码质量就慢慢提升了。同时,要制定代码规范,做好代码review,防止新的烂代码产生。

经验五:团队共识很重要

微服务拆分和代码重构,不仅仅是技术问题,也是人的问题。如果团队没有共识,有人支持,有人反对,那肯定做不好。

所以,在开始之前,要和团队充分沟通,让大家理解为什么要做,做了有什么好处,会有什么挑战。达成共识之后,再开始做。过程中,要定期同步进展,让大家看到成果,增强信心。

教训一:不要低估数据拆分的难度

我们一开始觉得,代码拆分是最难的,结果发现,数据拆分才是最难的。数据之间的关联和依赖,比代码复杂得多,而且数据迁移的风险很大,出了问题就是数据丢失或者不一致,后果很严重。

所以,数据拆分一定要谨慎,做好充分的准备,制定详细的迁移方案和回滚方案,充分测试,在业务低峰期迁移,迁移后仔细验证数据一致性。

教训二:不要在拆分的同时做大的功能变更

我们有一次,在拆分订单服务的同时,还做了一个大的促销功能变更。结果,拆分出了问题,促销功能也出了问题,两个问题搅在一起,排查了很久才解决。

所以,拆分和功能变更,尽量不要同时做。拆分的时候,只做拆分,不做功能变更。等拆分完成,稳定运行一段时间之后,再做新的功能。

教训三:做好文档和知识沉淀

拆分的过程中,我们踩了很多坑,也积累了很多经验。但是一开始没有做好文档和知识沉淀,导致后来的人又踩了同样的坑。

所以,过程中一定要做好文档和知识沉淀,把架构设计、拆分方案、踩坑经验、运维手册等,都写成文档,形成知识库,方便团队成员学习和参考。

结语

微服务拆分和代码重构,是我们团队做过的最有挑战的事情之一,也是最有价值的事情之一。

虽然过程很痛苦,踩了很多坑,但是结果是好的。现在,我们的系统架构清晰,代码质量高,开发效率快,系统稳定性好。团队的技术能力也提升了,大家对技术更有信心了。

如果你也在考虑做微服务拆分和代码重构,希望我们的经验能给你一些参考。记住,不要盲目跟风,要根据自己的业务和团队情况,做出理性的决策。渐进式拆分,小步重构,基础设施先行,团队共识,这些都是成功的关键。

最后,用一句话总结:"好的架构,不是设计出来的,而是演化出来的。好的代码,不是一次写出来的,而是持续重构出来的。"

愿每一个技术团队,都能拥有清晰的架构和优雅的代码,都能享受技术带来的乐趣。