大家好,我是老A
国庆节的时候,收到一位粉丝的私信求助。一来一回聊了半天,我发现了一个很多技术兄弟的通病------我们称之为「收藏夹式努力 」。水文看不上,只想让大佬推荐技术宝典,一旦拿到手,焦虑感削弱,宝典就在收藏夹里吃灰,下了班照样峡谷开黑。当你想督促他深入研究时,他又会用「他是大佬,我不是,我做不到 」来给自己设限,最终陷入「持续焦虑,持续躺平」的恶性循环。
这种惰性,是人之常情。但人和人之间的差距,就在于如何对抗它。这让我想起了我去年带过的一个外包兄弟,小汪。坦白说,他刚来的时候,也和这位粉丝一样,技术普通,满脸焦虑,总觉得自己不行。
但他最终,靠自己的努力,挣脱了这个循环。他没有转正,却在一年后,薪资翻倍进了字节。
今天,我就把小汪的这段真实经历,以及我当时给他设计的那份「镀金计划」,毫无保留地分享出来。希望能为所有困在原地的兄弟,提供一条走得通的路。
第一幕:一个困在围城的年轻人
小汪是2023年6月份进组的,主要跟我做CRM的售卖系统,给我的第一印象是这小伙子阳光开朗,踏实肯干,不懒惰,很勤劳,基本每天都是第一个到公司(当然不是说第一个到公司就好,主要是从这点看出他很勤劳)。
小汪的技术基础还行,大概半个多月就熟悉了公司的技术基础设施和基础业务,之后就开始跟着我做项目。
他的成长速度真的很快,每天都会追着我问各种业务问题,应用问题还有代码问题,每次CR也都主动找我交流,能看的出来,他是真的很想进步,所以我也是倾尽所有的去教他带他。前半年,他工作的热情高涨,每天都像个小太阳一样,正能量满满,积极乐观的工作态度感染着周围的同学。
但是半年后,明显感觉到这个小太阳能量不足了,脸上的笑容也越来越少,察觉到这个变化,我赶紧跟他进行了一次one one交流,让我终于知道了原因所在。
小汪表示他觉得自己就是个二等公民,作为外包,他感受到了诸多不便,再努力都没用。
-
不便一:各种权限申请搞疯掉
小到一篇文档、一个钉钉名片、一个项目环境吧器,大到一个代码库、应用、开关、配置他通通都要经过4层以上的审批,给他的工作带来了极大的不便,过程中也倍受冷眼,比如跟某些大厂骄子申请权限,已读不回,钉了也不回的例子太多,其中的心酸只有他自己能懂。
-
不便二:外包身份让他无法接触到核心
虽然我带着他做项目,能教的都在教他,业务也都给他讲的明明白白,但是从不会让他接触到项目最核心的技术部分,也不会把核心开发部分交给他做(这个我要喊一句冤枉,不是不交给小汪做,是不能,我们有严格的要求核心研发必须正式员工来做,毕竟背锅也是我们自己来,所以自己做也比较放心,但是每次的CR我都会叫上小汪,让他知道这个需求的核心部分是如何实现的)。
-
不便三:所有的需求都不会让他独立完成,都需要我出技术方案,他去执行
小汪觉得这样很不利于他的成长,就像离不开鸡妈妈的小鸡(
这里批评一下,这个比喻太不恰当了,我不是鸡妈妈😂)。后面这里我改为了他出方案,我来把关评审。 -
不便四:所有的核心技术分享都接触不到
小汪说他想成长,但是每次部门的核心技术分享都不会邀请他,他也不知道有哪些技术分享,这让他更加难受,想成长却没有门(
这个我后面反思了,因为我们的技术分享基本都在晚上8点以后,外包同学基本6点多就下班了,所以之前我一直没好意思留下小汪,毕竟让人加班等到8点不太优雅,那次one one谈话后我每次分享都会提前告诉他)
最后小汪终于道出了压在心底的话:他拼命工作、拼命学习,就是为了撕掉"二等公民"的标签,成为一个真正的"自己人"。但他抬头四顾,满眼都是绝望:"A哥,我来了一年多了,一个转正的都没见过。这条路,真的走得通吗?"
第二幕:"B面"点化------"你的目标,从一开始就错了"
听了他的话,我沉默了足足有半分钟。我不是在想怎么安慰他,我是在想,这盆冷水,到底该怎么泼下去,才不至于浇灭他眼里最后的光。
他说的这些,我何尝不知呢?别说他只来了一年多,我在公司6年了,也没见到一个外包转正的。外包转正,这本身就是一个悖论,可能性十不足一。
所以我直接告诉他了外包转正的三个"残酷真相":
- "HC(Headcount)隔离墙":首先外包在哪个公司都算是一种技术资源,哪里需要去哪里。其招聘成本和正式员工不在一个等级,一个部门每年的正式员工HC是很珍贵的,基本一年1-2个,所以老板和HR都想把这个名额给到一个尽可能优秀的人。那么老板如何判断一个候选人是否优秀?第一就要看学历和工作经历,基本95%的外包同学都倒在这一关了。而一个外包HC就宽松多了,老板向部门申请预算也会容易的多,所以体现在面试上也容易很多。这就是为什么外包在老板这关就很难转正的原因。
- "招聘标准双轨制" :坦诚地说,社招一个P6的门槛,和外包转正一个P6的门槛,是完全不同的。我们
社招的时候招一个P6的标准:985/211、有大厂经历、能独立完成中型项目、对自己做过的项目完全掌握这就行了。但是如果是外包转正一个P6:那你得有极其耀眼的项目经历或者做出过什么大贡献,比如挽回了1000万资损这种。但是这两个条件对于外包同学来说,难于上青天。。。这本身就是不公平的,却是赤裸裸的现实。 - "价值归属的原罪":外包同学是不会自己负责一块独立的业务的,所以他在项目里做的所有业绩,在汇报时天然会归功于他的直属正式员工Leader。
最后,我看着他的眼睛,一字一句地跟他说:"小汪,记住,从今天起,别再把转正当成你唯一的目标了。那是一条官方留给你,但几乎锁死的路。你的真正出路,是把在这里的每一天,都当成一场偷师学艺。你的目标,不是留下,而是镀金后,去一片更广阔的天空!"
第三幕:镀金!从CRUD Boy到"准架构师"的技术蜕变
1. 镀金第一步:"偷"文档,找到"战场"
老A点化 :我告诉小汪,你要学会"偷师",去lark上把我们CRM系统 所有的故障复盘文档 ,全都找出来读一遍。再去看架构组的周会纪要 ,看看那些P8、P9们,到底在为什么问题而头疼 。同时多交好一些正式员工,作为资源人脉,一旦有HC可以做内推。
发现"战场": 小汪花了一周的下班时间,真的找到了一个有价值的痛点------一个半年前的P3故障 复盘文档。这个故障始于CRM系统一个用于"同步客户签约状态 "的核心接口,由于需要依次调用 "会员中心"、"订购中心"、"风控中心"、"合同中心"四个下游服务,这几个系统都是有20年以上的历史的老系统,整个链路过长,在大促高并发下,TP99延迟飙升到5秒以上,导致整个签约流程的体验很差,经常出现签约异常情况。这个问题,因为"历史悠久、无人敢动",至今只是做了限流(令牌桶),也提出了渐进式优化的方案,但是苦于业务排期压力,一直没有排上日程。
老A点评 :"镀金 "的第一心法,找到那个所有人都知道痛,但没人敢治的"病灶 "。这,就是你的战场。
2. 镀金第二步:"偷"代码,找到"武器"
小汪找到了"战场",但却不知道该怎么解决。他能想到的,还是优化SQL、加缓存这些常规操作。
老A点化 :我直接把公司内另一个核心交易网关 的代码读权限,给他临时申请了一天。然后告诉他:"别看那些修修补补的代码。去看看我们最新一代的交易应用代码,看看他们是如何编排和调度多个下游微服务的。偷师他们的设计思想。"
"偷"到"圣经" :小汪在这份新代码里,第一次看到了一个完全不同的世界------基于CompletableFuture和ThreadPoolExecutor构建的、优雅的异步化、非阻塞的编排方案。他这才明白,高手们早就不用同步调用链这种办法了。
3. 镀金第三步:从"看懂"到"精通"的灵魂拷问
小汪看了一天的交易代码,然后兴奋地跑来找我,说他准备用CompletableFuture.allOf()来并行化那三个RPC调用。但他还没说完,我就提出了三个"B面"灵魂拷问,直接把他问住了:
第一问:你打算用什么线程池来跑这些异步任务?
小汪脱口而出:"就用CompletableFuture默认的就行吧?"
我告诉他,这就是新手和老兵的第一个分水岭 。默认的ForkJoinPool.commonPool(),就像一个顶级餐厅里那几个最牛的、做菜最快的大厨,它是为计算密集型任务设计的。
而你的RPC调用,是IO密集型任务,就像是去等一份永远不知道何时能送达的外卖。如果你让大厨去等外卖,整个厨房很快就会瘫痪。
正确的做法,是为这些IO任务,单独建立一个"服务员"线程池,让他们去等,别占用我们宝贵的大厨资源。
java
// 老A的B面架构第一课:为IO密集型任务自定义线程池
private final ThreadPoolExecutor syncExecutor = new ThreadPoolExecutor(
20, // 核心线程数,根据QPS和下游延迟估算
200, // 最大线程数,应对突发流量
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new LinkedBlockingQueue<>(1000) // 队列大小,防止OOM
);
第二问:三个RPC调用,真的可以完全并行吗?
我让他把旧代码贴出来,又问了他第二个问题:"你仔细看,这三个RPC调用,真的可以完全并行吗?" 小汪看着旧代码里那句orderClient.getLastOrder(info.getOrderId()),沉默了。
他这才发现,获取"订单信息",依赖于先获取"会员信息"的结果。
我告诉他,allOf是万马奔腾,适用于没有依赖关系 的并行任务。而这种需要"第一棒跑完,第二棒才能接力"的场景,你需要的是thenCompose<。这,是第二个分水岭。
java
// 老A的B面架构第二课:用thenCompose处理依赖性的异步任务
CompletableFuture<UserInfo> futureInfo = CompletableFuture.supplyAsync(...);
CompletableFuture<OrderInfo> futureOrder = futureInfo.thenCompose(info ->
CompletableFuture.supplyAsync(() -> orderClient.getLastOrder(info.getOrderId()), syncExecutor)
);
第三问:你准备用 thenAccept还是 thenAcceptAsync来更新数据库?
最后,我问了他第三个问题:"当所有结果都计算完,你准备用thenAccept还是thenAcceptAsync来更新数据库?" 小汪一脸茫然。
我给他讲了一个真实的"B面"血泪史:曾经一个团队,因为在回调里使用了同步的thenAccept,而回调方法里又有一个微小的锁竞争,导致在高并发下,整个异步线程池被"反向"阻塞,引发了P2故障。
这,是第三个分水岭 ------ 永远要假设你的回调逻辑也可能阻塞,用thenAcceptAsync把它也扔进线程池里去执行,做到"绝对隔离"。
4. 终极镀金:用"A面"武器,重构"B面"屎山
在我问出这三个问题,并把自定义线程池和ThenCompose这两个武器的核心代码画给他看之后,小汪说他要回去思考下。
我知道,他已经悟了,聪明的小汪~
又过了大概一周后,小汪发来了一个重构方案评审会邀,在会上他向我展示了他利用下班和周末的时间,写出的下面这份堪称"教科书级"的重构方案。
他不仅完美地应用了我们讨论的所有技术点,甚至还举一反三,在方案里加入了极其专业的超时控制(orTimeout)、基于指数退避的重试机制、以及完善的异常处理。
他不再是简单地模仿,而是真正理解了系统痛点背后的"B面"权衡。
我很惊讶于小汪的执行力,可以这么快就给出这样比较完整的方案。我对整体的重构方案进行了严格的技术把关和评估,最终评审通过,决定采用小A的这套方案 ,并通知小A配合测试同学给出压测方案和报告。 团队双周技术分享会上,小汪主讲了这个重构方案,并给出了详尽的压测报告(P99延迟从5秒降到150ms,接口吞吐量提升300%),整个过程技惊四座。那一刻,再也没有人把他当外包了。
java
@Service
public class UserSyncServiceV2 {
// 老A的B面架构第一课:为IO密集型任务自定义"服务员"线程池
private final ThreadPoolExecutor syncExecutor = new ThreadPoolExecutor(
20,
200,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000)
);
// ... 注入各个Client ...
public CompletableFuture<Boolean> syncStatusAsync(Long customerId) {
// 1. 并行获取无依赖的用户信息和风控结果
CompletableFuture<UserInfo> futureInfo = CompletableFuture.supplyAsync(
() -> userClient.getUserInfo(customerId),
syncExecutor
);
CompletableFuture<RiskResult> futureRisk = CompletableFuture.supplyAsync(
() -> riskClient.checkRisk(customerId),
syncExecutor
);
// 2. 老A的B面架构第二课:用thenCompose处理依赖性的异步任务(订单依赖用户信息)
CompletableFuture<OrderInfo> futureOrder = futureInfo.thenCompose(info -> {
if (info == null || info.getOrderId() == null) {
// 如果前序结果为空,优雅地返回一个空的Future
return CompletableFuture.completedFuture(null);
}
return CompletableFuture.supplyAsync(
() -> orderClient.getLastOrder(info.getOrderId()),
syncExecutor
);
});
// 3. 老A的B面架构第三课,组合所有结果,并用thenApplyAsync进行最终的数据库操作
return CompletableFuture.allOf(futureOrder, futureRisk)
.thenApplyAsync(v -> {
// ...从各个Future中join()结果,并进行组合...
// ...验证数据完整性...
// 更新数据库
updateDatabase(syncResult);
log.info("用户状态同步成功, customerId: {}", customerId);
return true;
}, syncExecutor)
.exceptionally(e -> {
log.error("异步同步失败, customerId: {}", customerId, e);
return false;
});
}
// ... updateDatabase()等私有方法 ...
}
老A说:从毕业作品到"生产级"的最后三公里兄弟们,小汪的这份代码,已经是一个极其优秀的技术验证原型。但要把它真正投入生产,我们还需要打磨三个"B面"细节:
线程池的哨兵: 必须为这个线程池配置合理的拒绝策略,并接入相关监控平台(如Sunfire)进行监控,防止在高并发下被打爆。
异步安全保障: exceptionally里的简单日志还不够。生产级代码需要集成Resilience4j这样的熔断器和重试框架,防止下游服务的抖动引发雪崩。
数据的安全锁: 异步化后,如何保证数据一致性?我们需要引入Saga或TCC等分布式事务方案,来确保整个流程的原子性。
这最后三公里,才能区分出你是高级工程师 还是架构师
第四幕:一年之后------"他放弃了转正的想法,选择了字节"
就这样,小汪按照我的镀金计划不断的"偷师取经,稳步成长",我能明显感觉到他在技术、视野和自信心上发生了脱胎换骨的变化。今年年初,我们的一个正式HC恰好空出,老板也确实在考虑他。但与此同时,通过之前积攒来的人脉,他也拿到了一个字节的面试机会。结果也在我的意料之中,他顺利通过了面试,薪资直接翻倍。他最终礼貌地"放弃"了在内部等待那个不确定的转正机会,飞到了另一片天空。
感言
我至今都记得小汪拿到Offer后请我吃饭时说的话:"A哥,谢谢你,没有你就没有我的今天。谢谢你让我找到成长的最优路径,谢谢你掏心窝子对我说的那些话,谢谢你对我的倾囊相授,谢谢你让我明白成长的意义所在,谢谢你让我知道我可以,我能行!"
老A说:其实一个人的价值,不应该由别人给你打上的身份标签来定义。我们可能无法选择起点,但可以通过正确的战略和努力,选择自己的终点。
老A时间
感谢各位兄弟的阅读。
我是老A,一个只想跟你说点B面真话的师兄。如果这篇文章让你有了一点点启发,那就是对我最大的肯定。
为了感谢大家的支持,我把这两年在一线大厂面试和带团队中,沉淀下来的所有上不了台面的私房笔记,整理成了一份《程序员B面生存手册》。
里面没有市面上千篇一律的八股文,只有一些极其管用的"潜规则"和"避坑指南",希望能帮你少走一些弯路。
关注我的同名公众号【大厂码农老A 】,在后台回复"B面",就能免费获取。
回复"简历 "获取《简历优化手册》
回复"arthas "获取史上最全的《大厂arthas实战手册》
回复"指导 "获取《大厂外包镀金手册》
最后,如果觉得内容还行,也希望能点个赞、点个在看,让更多需要它的兄弟看到。
我们一起,在技术的路上结伴"陪跑"。