云计算百科
云计算领域专业知识百科平台

Java 老项目该不该拆微服务?89处交叉引用、0处FeignClient 的实测答案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

老炮踩坑录 · V07 · 老炮视野系列 基于「企业融合评估平台」真实源码,回答一个我被问了三次的问题 关键词:单体架构 · 微服务拆分 · 模块耦合 · 分布式成本

👋 欢迎阅读

在这里插入图片描述

🏠个人主页: 知守观 📘我的专栏: 老炮踩坑录 💻当前内容:微服务拆分方法论

文章目录

    • 前言
    • 这个单体长什么样
    • 四个包之间的依赖关系
    • 拆分的基础条件
    • 真拆的话,要改动哪些地方
    • 什么情况我才会同意拆
    • 不拆,但要把模块依赖理顺
    • 老炮点评

前言

这个系列写到第七篇,也快接近尾声了。

这段时间我被问得最多的问题是:这个项目当年为什么是单体,怎么不拆成微服务呢?

问的人多是工作三五年的同行。在他们的认知里,2022 年上线的项目还打包成单体,多少有点说不过去。还有个前同事告诉我,新来的技术负责人到岗第一周就出了拆分方案,Nacos、Gateway、Sentinel 等等这些主流中间件一样不落,他在方案里说"单体架构已无法支撑业务的发展"。

我听完没什么反应。类似的方案我见过不止一份,理由翻来覆去就那么几条。

不是说我反对微服务,我也拆过,也接手维护过别人拆的。我只是对"该不该"这三个字有生理性警惕——它前面还有一个更根本的问题:为什么要拆。

今天我们就拿这个老项目当样本,把拆分的成本一项项算清楚。事先说明,所有数字都是我从源码里实际数出来的,但我离职两年了,团队现在几个人我是听说的,不敢打包票。

这个单体长什么样

先交代一下这个项目的基本情况。

这个项目是一个 WAR 包,外置 Tomcat,两台机器,前面有负载均衡组件。224 个 Java 文件,30 个 Controller,33 个 Mapper。就这个体量,放任何一个做过电商的人眼里,这叫小项目。

业务的使用规律也要交代一下。企业融合评估平台,注册期企业集中填报,职能部门逐级推荐,后台审核出报告,一个批次走完就歇一阵。平时没多少流量,忙也就忙那几周。

代码里有个数字比任何文档都诚实。鉴权切面里登录态缓存的容量:

// AuthAspect.java
private static final Cache<String, Object> CACHES = CacheBuilder.newBuilder()
.maximumSize(50)
.expireAfterWrite(30, TimeUnit.MINUTES)
.build();

maximumSize(50)。当年写这行代码的人,给同时在线用户留的容量是 50。这个数可能是拍的,但它反映了当年团队对并发的预期,而且从没出过事。一个按几十并发设计的批次型政务平台,跟"无法支撑业务发展"之间隔着好几个量级。

顺带说一句,两台机器早就说明这单体做过水平扩展了。Java 雪花算法 ID 重复排查 这篇里写过雪花 ID 翻车,两台机器 workerId 配成一样,生成重复 ID——那次事故反过来证明部署形态就是双机分摊,没有单机硬扛。

四个包之间的依赖关系

项目按业务分了四个包:sys(系统管理)、rapply(报名申请)、elec(电子申报)、common(公共)。包结构看起来挺讲道理。有一天下午我闲着无事,想看看这三个业务包之间互相依赖到什么程度,就 grep 了一把 import,结果如下:在这里插入图片描述

  • rapply 引用 sys 的地方,48 处。
  • elec 引 rapply,16 处。
  • sys 引 rapply,10 处。
  • elec 引 sys,8 处。
  • rapply 引 elec,5 处。
  • sys 引 elec,2 处。

加起来 89 处,六个方向全有——理论上三个包最多也就六个依赖方向。

这就是包级别的循环依赖。

数字是一方面,更难看的是引用的层次。按理说模块之间要调,也该调对方的 Service 接口。rapply 不管这个,它直接注入 sys 的 Mapper:

// rapply 的 ApplyInfoDeptServiceImpl
import com.xiaomayi.sys.mapper.SysAreaMapper;
import com.xiaomayi.sys.mapper.SysEnterpriseTypeMapper;
import com.xiaomayi.sys.mapper.SysIndustryMapper;

绕过人家的 Service 层,直接查人家的表。

elec 这边更过分:

// elec 的 ApplyElecInfoServiceImpl
import com.xiaomayi.rapply.mapper.ApplyEnterpriseBaseinfoMapper;
import com.xiaomayi.rapply.service.EnterpriseRegistService;
import com.xiaomayi.rapply.service.impl.HuaweiyunFileServiceImpl;

Mapper 直接用,接口也用,最后一行把对方的 ServiceImpl 实现类 都 import 进来了,连实现类都不放过呀。

这 89 处意味着什么?真拆成微服务,每一处都得做个决定:改成远程调用,还是数据冗余一份,还是干脆把表挪过去。但是没有哪个决定是轻松的。

拆分的基础条件

拆微服务需要哪些基础,我顺手查了一遍。

grep -r "@FeignClient\\|@Reference" src/ # 0 处
grep -r "RabbitTemplate\\|RabbitListener" src/ # 0 处

远程调用,零。消息队列,零。全项目没有任何两个服务之间通信的基础。

有意思的是 pom.xml 里躺着这么个东西:

<!– pom.xml –>
<!–rabbitMq –>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

RabbitMQ 依赖加进来了,全项目一条消息没发过。我猜是当年有人"为以后拆服务做准备"留的。三年过去,依赖躺在那儿,准备工作一项没做。

这种情况在拆分改造里太常见了:以为加了依赖就算铺了路,其实路还是那条路。

真拆的话,要改动哪些地方

上面还是铺垫。真把 PPT 方案落地,动的是这些:

登录态得重做。 现在的认证是 Cookie + Token + Guava 本地缓存,用户信息存在 JVM 内存里。单体双机已经在靠负载均衡的会话保持硬撑了。拆成五个服务,每个服务都得验身份,要么上 Redis 集中存 Token、网关统一鉴权,要么五个服务各写一套。现在那个 AuthAspect 切面拦的是进程内的 Controller,拆完之后它谁也拦不住。

89 处调用逐个处理。 rapply 直查 sys 的表这种,要么 sys 暴露接口,要么两个服务共库——微服务里最丑的那种共库。拆之前这是一次进程内调用,纳秒级;拆之后是网络调用,附赠序列化、超时、重试、熔断一整套。89 处,每一处。

事务的处理方式全变了。 全项目 14 处 @Transactional,报名、诊断、报告生成,全是跨模块写数据。本地事务拆完就是分布式事务。我为什么对这单没信心?Transactional 事务失效排查这篇中写过,这个项目连本地事务都用错过——catch 吞了异常导致回滚静默失效,查了三天。连本地事务的坑都没填利索,直接上 Seata,我劝你冷静。

定时任务也要处理。 2 个 @Scheduled,双机部署本来就有重复执行的风险,拆完要么分布式调度,要么分布式锁。收益为零,工作量实打实。

运维翻倍。 现在是两个 Tomcat,日志在本地磁盘,出问题 ssh 上去 tail 一下。拆完是五六个服务加注册中心、配置中心、网关、链路追踪,每个组件都要有人装、有人盯、有人半夜爬起来处理告警。

最后是人。我离职时后端三四个人,听前同事的意思,现在也没扩编。三四个人守五个微服务外加一整套中间件,平均每人摊不到一个服务,但每个人都得全懂。

什么情况我才会同意拆

我拆过微服务,所以我知道什么情况出现时值得拆。

  • 一个是人多到互相挡路。十几个后端挤在一个代码库里,改个 pom 要等三个人 review,发布窗口抢破头——这时拆的是协作边界,拆完各玩各的。这个项目三四个人,不存在。

  • 一个是某个模块发布频率远高于其他,每次发布都拖着全站重启。这个项目按批次迭代,整体一起发,也不存在。

  • 一个是某个模块的资源消耗拖垮整体。这个项目确实有压力点,在导出——POI 生成大 Excel、HTML 转 PDF,一跑 CPU 就满,百万级数据导出OOM解决 这篇中写过 OOM 是怎么来的。但这个有便宜得多的解法:导出任务扔消息队列异步跑,RabbitMQ 依赖都躺三年了,正好用上;或者同一份 WAR 单独部署一台导出专用节点。都不需要微服务框架。

  • 还有一个是模块边界天然干净、数据交互少,拆起来不伤筋动骨。这个项目有 89 处交叉引用,这条显然也不满足。 四种情况,这个项目一种都不占。

  • 不拆,但要把模块依赖理顺

    我的结论就两个字:不拆。

    但"不拆"不能是句号,问题本身还在那儿。这 89 处交叉引用本身就是债,单体状态下也得还——还的方向是收敛成单向依赖: 在这里插入图片描述

    sys 是底座,rapply 和 elec 互不引用。rapply 直插 sys Mapper 的那些调用,收回到 sys 的 Service 接口后面;elec 引用别人 ServiceImpl 的地方,改成依赖接口。

    这笔债不还,"拆微服务"就只是把进程内的乱搬到网上。以前循环依赖编译器帮你报错,拆完之后,是半夜的服务超时找你聊天。

    哪天这些真做完了,依赖方向理顺了,登录态抽象出来了,模块边界干净了——那时候团队要是真扩到十几个人、业务真涨起来了,再拆,是顺水推舟的事。单体收拾得干干净净,想拆随时能拆。单体本身的依赖要是乱的,拆成微服务之后还是乱的,只不过调用方式从进程内调用变成了远程调用。

    老炮点评

    写到最后,说句掏心窝的话。

    微服务这剂药,治的是组织的病:团队大、发布互相挡、节奏打架。这个项目得的是代码的病——1600 行的 Controller、ThreadLocal 串用户数据、事务吞异常不回滚。这些病拆成微服务一个都治不了,1600 行不会因为搬进"服务"就变短,它只会变成一个有 1600 行代码的微服务,外加分布式事务和跨服务排查链路。

    微服务治的是组织的病,不是代码的病

    所以再有人问你"这个项目该不该拆",别急着答,先问回去:为什么要拆?

    答得上具体的——哪个模块、挡了谁的发布、拖垮了谁的资源——一条条对,对得上再谈拆。答不上来,只会一句"单体不先进了",那多半只是想要简历上多一行 Spring Cloud 技术栈。


    扩展阅读 如果你还没看过这个项目的技术选型铁律,可以翻一下《技术选型的三条铁律》。


    下期预告:《文件上传限制 1024MB,运维找上门了》

    spring.servlet.multipart.max-file-size 配成 1024MB 那天起,服务器的磁盘告警就没停过。上传限制不是越大越好,到底该按什么定、怎么定,我们下期聊。

    如果本文对你有帮助,欢迎:

    👍 点赞 | ⭐ 收藏 | 👤 关注 | 💬 留言。

    你的每一次点赞鼓励都是我继续更新的动力,我们下一篇见!🚀

    我是老炮,18 年 Java 老兵,仍在一线。关注「Java老炮踩坑录」,不错过每一篇真实案例,少踩坑。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Java 老项目该不该拆微服务?89处交叉引用、0处FeignClient 的实测答案
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!