这坨屎山,我接了
大家好,我是老A。
我想很多程序员有过这种经历,新接手一个项目,打开工程一看,妥妥的一大坨🤦,内心OS:好嘛,又要“屎山雕花”了。。。
我这两年在做电商业务,所以业务上经常会搞大促,3天一小促,5天一大促,作为技术早就习惯了这种研发节奏(倒排
)。今年6月是我们业务年中的一次大型大促,所以5月份的需求爆炸多,基本都是倒排,业务天天拿着大喇叭在我们屁股后喊📢:这个需求不做就会死。。。
在需求集中爆发的这个月,我们团队接到一个关于“虚拟库存预占”的紧急需求,必须在一个祖传的核心订单模块(屎山)里实现。这个模块是出了名的“shit”,代码三年没人敢动,文档约等于没有,最后一任维护者已经退隐江湖了。。。
PRD评审会上,技术这边一片寂静,眼神里充满了“you can you up,我家还有事”的谦让。
很不幸,由于另外几个大神都有不接这个需求的“合理”理由,所以这个需求顺理成章的落在了我的头上,shit!!!
技术调研阶段我整整花了两天时间,在屎山里深潜、考古、通灵,最后终于被我摸清了它的基本脉络。过程中发现了一个被滥用多年的ThreadLocalContextHolder
,可谓是这坨屎山的精髓所在
,这东西就像个幽灵,把user id在几十个方法调用之间“隔空投送”,代码的可读性和可测性约等于零。
此刻我面临一个两难抉择:
- 理想主义方案:花两周时间,彻底重构整个模块,废弃
ThreadLocal
。这样做最正确,但业务方和产品肯定会提刀来见。 - 务实主义方案:搞一个“临时止血”方案,只写好我自己的增量代码,在与旧模块交互时,临时兼容一次那个
ThreadLocal
,确保3天内能上线。
经过深思熟虑后我选了方案2。我知道这个选择,在CR评审会上肯定会受到质疑和挑战,甚至可能会上演一场好戏,但是依然这么还是这么选择了。
最终我提交的CR,核心增量代码如下(仅截取部分调用了那个ContextHolder的核心代码):
@Service
public class StockValidationService {
@Autowired
private SkuRepository skuRepository;
/**
* 6月大促新增的核心功能:校验预占库存的合法性。
*
* @param skuId 商品ID
* @param quantity 预占数量
* @return 是否校验通过
*/
public boolean validatePreOccupation(String skuId, Integer quantity) {
if (StringUtils.isBlank(skuId) || Objects.isNull(quantity) || quantity <= 0) {
throw new BizException(INVENTORY_PARAM_ILLEGAL.getCode, INVENTORY_PARAM_ILLEGAL.getDesc);
}
// ...此处省略10行业务逻辑,比如风控校验、活动规则校验等...
// 为了获取当前操作的用户信息,进行风险等级判断不得不调用了这个祖传的ContextHolder
Long currentUserId = ContextHolder.getCurrentUserId();
if (isHighRiskUser(currentUserId)) {
// ...执行高风险用户校验逻辑...
}
Integer stock = skuRepository.getStock(skuId);
return stock >= quantity;
}
private boolean isHighRiskUser(Long userId) {
// ...根据用户ID判断是否为高风险用户的逻辑...
return false;
}
}
第二幕:CR评审会上的“公开处刑”
我们需求上线前都要经过严格的代码评审,所以在上线前一周我就已经预约好了架构组和主管们的时间,在上线前3天的一个上午开始了代码评审会,会议室里,技术主管和主管的主管都列席旁听。轮到我负责的部分,那位以代码洁癖闻名、眼里容不得沙子的架构师大B哥,开启了他的挑战。
他没有看我的增量代码细节,而是顺着那一行ContextHolder.getCurrentUserId()
的调用,直接点开了ContextHolder
这个祖传类。
他冷笑一声,没有看我,而是转向在场的技术主管们说:“领导,我有点不明白。都2025年了,我们团队的代码库里,为什么还会允许这种反模式存在? ”
然后,他才把头转向我,音量不大,但整个会议室都听得见:
“小A,你这次提交的代码,就像是在一锅馊了的汤里,加了一勺松茸。汤还是馊的,松茸也被污染了。 我说的不是你写的这几行业务逻辑,我说的是你选择与这坨屎共存的这个决策本身,这个决策,就很屎!
并且你这个方案太脏了!一个合格的设计,至少应该做到单一职责和依赖倒置!你应该把风险校验抽成一个RiskService,把活动规则抽成一个RuleEngine,把输入输出都用DTO封装起来,像这样……”
接着大B就开始在白板上大展鸿图,画了一个大致的类图。
我能感觉到肾上腺素瞬间飙升,脸颊发烫,虽然早有预料到会被质疑和挑战,但是这个B说的话是真难听啊,不愧是大B。
第三幕:B面反击——“三板斧,让布道师回归现实”
第一招:釜底抽薪——“您说的都对,但这坨屎,是三年前的”
面对这种“公开处刑”,我没有慌乱,因为这种情况也算是预料之中的,我整理了一下思绪,点了点头:“B哥,您对ThreadLocal滥用
的所有批评,我100%同意。事实上,这坨屎比您说的还臭。它的可测试性几乎为0,在异步场景下就是P0级故障的定时炸弹。 ”
我又看了看白板上那张近乎“完美”的架构图,平静地反问:“B哥,您这套方案,非常完美,但为了实现它,我们需要新增8个类,修改5个模块,联调3个下游,还需要一周的完整回归测试。而我这次任务的工期,只有3天。”
“所以,我们今天讨论的,是如何用最短的时间最小的代价完成业务诉求,我的临时方案,虽然丑但是能保证业务活下去。而您画的这张图,是极其完美主义,所有程序员都向往的存在,如果给我足够的时间,我十分愿意这样去实现”
接着我在投屏打开了SonarQube的扫描报告页面,话锋一转:“B哥,您可以看下SonarQube报告,你之前说的这些问题,其实我在做需求的时候,就已经全部了解到了,可是我们没时间去重构啊。大家可以看看这个类的最后修改时间,是2022年。我这次的任务,是在3天内把这个大促业务需求完成,这个工程本身就像一栋马上要塌的危楼,我的目标是加一个消防通道,确保大促的业务能活下去。我是在救火
,不是在装修
。”
第二招:灵魂拷问——“如果让你来选,是‘死’还是‘脏’?”
我不给大B喘息的机会,直接把问题抛给他:“所以,当时我面临一个选择:
A,花两周时间彻底重构,让代码变得优雅,但业务延期,大促功能上不了线。
B,用一个‘临时止血’方案,兼容这坨屎,3天上线,保证业务成功。 ”
我看着B哥,真诚地提问:“B哥,我想请教一下,如果这个决策是您来做,您会选择让业务‘优雅地死’,还是‘肮脏地活’? ”
第三招:更进一步——“我不仅想到了,我还验证过了”
在对方陷入沉默时,我继续输出:“当然,我不是只想肮脏地活。作为一个工程师,我也有洁癖。在评估临时方案的同时,我也在思考,重构这块到底需要多大代价?会不会有什么我们没想到的坑?所以,我利用下班时间,针对最核心的逻辑,做了一个技术验证原型。
这个POC证明了,采用显式参数传递的方案是完全可行的,并且对下游的改造成本是可控的。我的结论是,完整的重构,预估需要80人日。只要这次大促的需求上线,我随时可以带人去拆弹。
我的CR,结束了。”
重构后的代码
@Service
public class StockValidationService {
@Autowired
private SkuRepository skuRepository;
/**
* 重构后的版本:所有依赖都通过“显式参数传递”,方法变成了一个纯粹、可预测的函数
*
* @param skuId 商品ID
* @param quantity 预占数量
* @param currentUserId 核心变化-当前用户ID被作为参数明确地传递进来
* @return 是否校验通过
*/
public boolean validatePreOccupation(String skuId, Integer quantity, Long currentUserId) {
if (StringUtils.isBlank(skuId) || Objects.isNull(quantity) || quantity <= 0) {
log.wanrn("xxx");
throw new BizException(INVENTORY_PARAM_ILLEGAL.getCode, INVENTORY_PARAM_ILLEGAL.getDesc);
}
if (StringUtils.isBlank(currentUserId)) {
log.wanrn("xxx");
throw new BizException(USER_STATUS_ILLEGAL.getCode, USER_STATUS_ILLEGAL.getDesc);
}
// ...同样的业务逻辑...
if (isHighRiskUser(currentUserId)) {
// ...执行高风险用户校验逻辑...
}
Integer stock = skuRepository.getStock(skuId);
return stock >= quantity;
}
private boolean isHighRiskUser(Long userId) {
// ...根据用户ID判断是否为高风险用户的逻辑...
return false;
}
}
B哥笑了笑说:“改个ThreadLocal而已,能有多大成本?你这是在为自己的妥协找借口罢了。”
“B哥,如果只是单纯改代码,确实很快。但在做这个POC时我也对整个重构的成本,做了一次完整的评估。这是我的评估报告,我边说边将投屏切换到了我的成本分析页:
影响面分析与方案设计 (10 人/天)
依赖分析:ContextHolder.getCurrentUserId()这个“幽灵”到底飘散在多少个类、多少个方法里?我们需要用IDE的静态分析工具,完整地画出一张“依赖地图”。链路梳理:这些被调用的方法,又被多少上游的业务方(Controller, Job, MQ Consumer)所依赖?整个调用链路有多深?
方案评审:设计出的新版接口和参数传递方案,需要组织多个相关团队进行评审,达成共识。
代码重构与单测修复 (20 人/天)
代码修改:这才是真正的“体力活”,修改几十上百个文件的方法签名和调用。单元测试重构:所有依赖了ContextHolder的旧单元测试,现在全部要重写。因为依赖了“全局状态”,这些测试本身可能就很脆弱,修复成本极高。
全链路回归测试 (40 人/天)
自动化测试:需要QA同学配合,修改所有覆盖到相关链路的自动化测试用例。
手动测试:由于改动的是核心模块,可能会引发意想不到的“蝴蝶效应”。QA团队必须对所有相关业务场景,进行一次彻底的、地毯式的“人工回归测试”,确保没有产生新的Bug。这个过程极其耗时。
灰度发布与线上观察 (10 人/天)
发布策略:这种核心改动,绝不可能“一键全量上线”。必须制定周密的灰度发布计划(比如先上1%的机器,再到10%,再到50%),并准备好随时可以回滚的预案。
线上观察:在灰度发布期间,需要有专人(开发、QA、SRE)24小时盯着监控大盘,观察业务指标、系统性能是否有异常波动。
所以真正的成本大头,在测试和发布这两个环节。要动这个核心模块,我们至少需要协调3个上下游团队,并需要测试团队投入至少一个月的回归测试资源。这个成本,不是我一个人利用下班时间就能覆盖的。”
重构的理论支撑
1⃣️《Effective Java》的告诫
Joshua Bloch 在《Effective Java》Item 57 中强调:“Minimize the scope of local variables”(最小化局部变量的作用域)。
解读:ThreadLocal本质上是将一个变量的作用域,从“方法内”扩大到了“整个线程的生命周期”,这是一种“作用域滥用”。一个优秀的API设计,应尽可能减少这种对外部隐式状态的依赖。
2⃣️ 框架之魂:Spring Framework 的设计哲学
Spring框架的核心,是“依赖注入”(Dependency Injection)。
解读:Spring花了20年时间,教育了全世界的Java开发者——“你需要什么,就通过构造函数或方法参数明确地告诉我,不要让我去一个全局的地方自己找”。ContextHolder这种做法,完全违背了IoC和DI的初衷。
3⃣️ 未来之势:异步/虚拟线程的“天敌”
Oracle官方在Java 21虚拟线程(Project Loom)的JEP 444文档中明确指出: “Avoid thread-local variables... thread-local variables are a significant obstacle to scalability.”(避免使用线程局部变量...它是可伸缩性的巨大障碍)
解读:在未来的虚拟线程模型下,一个请求可能会在多个不同的底层OS线程上执行。ThreadLocal这种强依赖物理线程的模式,会彻底失效,并带来难以追踪的Bug。继续使用它,等于是在为未来的技术升级埋下最深的雷。
曲终人散——CR,过
会议室里鸦雀无声。B哥看了看我,又看了看各位主管,最后点点头,说:“优化这块进7月迭代排期池吧。这次CR,过。”
会议结束后,我脑子里盘旋着一个想法:Code Review的最高境界,不是找茬
,而是共识
。一个优秀的工程师,不仅要能写出干净的代码,更要有勇气和智慧,去推动团队偿还应用的历史债务。
这,可能就是“B面”工作的常态吧。
福利时间
为了感谢大家的支持,我把这两年在一线大厂面试和带团队的过程中,沉淀下来的所有的私房笔记,整理成了一份《大厂码农老A的B面真话手册》。
里面包含了我这么多年在职场摸爬滚打总结下来的职场成长经验、潜规则和避坑指南,当然也有我总结的很多求职经验、各类经典题目以及我的独门心得。
关注我的同名公众号【大厂码农老A】,后台回复“B面”你会发现新大陆。
最后,如果觉得内容还行,也希望能点个赞、点个在看,让更多需要它的兄弟看到。感谢大家!