大家好,我是老A
國慶節的時候,收到一位粉絲的私信求助。一來一回聊了半天,我發現了很多技術兄弟的通病——我們稱之為「收藏夾式努力」。水文看不上,只想讓大佬推薦技術寶典,一旦拿到手,焦慮感削弱,寶典就在收藏夾裡吃灰,下班照樣峽谷開黑。當你想督促他深入研究時,他又會用「他是大佬,我不是,我做不到」來給自己設限,最終陷入「持續焦慮,持續躺平」的惡性循環。
這種惰性,是人之常情。但人和人之間的差距,就在於如何對抗它。這讓我想起了我去年帶過的一個外包兄弟,小汪。坦白說,他剛來的時候,也和這位粉絲一樣,技術普通,滿臉焦慮,總覺得自己不行。
但他最終,靠自己的努力,掙脫了這個循環。他沒有轉正,卻在一年後,薪資翻倍進了字節。
今天,我就把小汪的這段真實經歷,以及我當時給他設計的那份「鍍金計畫」,毫無保留地分享出來。希望能為所有困在原地的兄弟,提供一條走得通的路。
小汪是2023年6月份進組的,主要跟我做CRM的販賣系統,給我的第一印象是這小夥子陽光開朗,踏實肯幹,不懶惰,很勤勞,基本每天都是第一個到公司(當然不是說第一個到公司就好,主要是從這點看出他很勤勞)。
小汪的技術基礎還行,大概半個多月就熟悉了公司的技術基礎設施和基礎業務,之後就開始跟著我做項目。
他的成長速度真的很快,每天都會追著我問各種業務問題,應用問題還有代碼問題,每次CR也都主動找我交流,能看得出來,他是真的很想進步,所以我也是傾尽所有的去教他帶他。前半年,他工作的熱情高漲,每天都像個小太陽一樣,正能量滿滿,積極樂觀的工作態度感染著周圍的同學。
但是半年後,明顯感覺到這個小太陽能量不足了,臉上的笑容也越來越少,察覺到這個變化,我趕緊跟他進行了一次one one交流,讓我終於知道了原因所在。
小汪表示他覺得自己就是個二等公民,作為外包,他感受到了諸多不便,再努力都沒用。
不便一:各種權限申請搞瘋掉
小到一篇文檔、一個釘釘名片、一個項目環境吧器,大到一個代碼庫、應用、開關、配置他通通都要經過4層以上的審批,給他的工作帶來了極大的不便,過程中也倍受冷眼,比如跟某些大廠驕子申請權限,已讀不回,釘了也不回的例子太多,其中的心酸只有他自己能懂。
不便二:外包身份讓他無法接觸到核心
雖然我帶著他做項目,能教的都在教他,業務也都給他講的明明白白,但是從不會讓他接觸到項目最核心的技術部分,也不會把核心開發部分交給他做(這個我要喊一句冤枉,不是不交給小汪做,是不能,我們有嚴格的要求核心研發必須正式員工來做,畢竟背鍋也是我們自己來,所以自己做也比較放心,但是每次的CR我都會叫上小汪,讓他知道這個需求的核心部分是如何實現的)。
不便三:所有的需求都不會讓他獨立完成,都需要我出技術方案,他去執行
小汪覺得這樣很不利於他的成長,就像離不開雞媽媽的小雞(這裡批評一下,這個比喻太不恰當了,我不是雞媽媽😂)。後面這裡我改為了他出方案,我來把關評審。
不便四:所有的核心技術分享都接觸不到
小汪說他想成長,但是每次部門的核心技術分享都不會邀請他,他也不知道有哪些技術分享,這讓他更加難受,想成長卻沒有門(這個我後面反思了,因為我們的技術分享基本都在晚上8點以後,外包同學基本6點多就下班了,所以之前我一直沒好意思留下小汪,畢竟讓人加班等到8點不太優雅,那次one one談話後我每次分享都會提前告訴他)。
最後小汪終於道出了壓在心底的話:他拼命工作、拼命學習,就是為了撕掉“二等公民”的標籤,成為一個真正的“自己人”。但他抬頭四顧,滿眼都是絕望:“A哥,我來了一年多了,一個轉正的都沒見過。這條路,真的走得通嗎?”
聽了他的話,我沉默了足足有半分鐘。我不是在想怎麼安慰他,我是在想,這盆冷水,到底該怎麼潑下去,才不至於澆滅他眼裡最後的光。
他說的這些,我何嘗不知呢?別說他只來了一年多,我在公司6年了,也沒見到一個外包轉正的。外包轉正,這本身就是一個悖論,可能性十不足一。
所以我直接告訴他了外包轉正的三個“殘酷真相”:
“HC(Headcount)隔離牆”:首先外包在哪個公司都算是一種技術資源,哪裡需要去哪裡。其招聘成本和正式員工不在一個等級,一個部門每年的正式員工HC是很珍貴的,基本一年1-2個,所以老闆和HR都想把這個名額給到一個儘可能優秀的人。那么老闆如何判斷一個候選人是否優秀?第一就要看學歷和工作經歷,基本95%的外包同學都倒在這一關了。而一個外包HC就寬鬆多了,老闆向部門申請預算也會容易的多,所以體現在面試上也容易很多。這就是為什麼外包在老闆這關就很難轉正的原因。
“招聘標準雙軌制”:坦誠地說,社招一個P6的門檻,和外包轉正一個P6的門檻,是完全不同的。我們社招的時候招一個P6的標準:985/211、有大廠經歷、能獨立完成中型項目、對自己做過的項目完全掌握這就行了。但是如果是外包轉正一個P6:那你得有極其耀眼的項目經歷或者做出過什麼大貢獻,比如挽回了1000萬資損這種。但是這兩個條件對於外包同學來說,難於上青天。。。這本身就是不公平的,卻是赤裸裸的現實。
“價值歸屬的原罪”:外包同學是不會自己負責一塊獨立的業務的,所以他在項目裡做的所有業績,在匯報時天然會歸功於他的直屬正式員工Leader。
最後,我看著他的眼睛,一字一句地跟他說:“小汪,記住,從今天起,別再把轉正當成你唯一的目標了。那是一條官方留給你,但幾乎鎖死的路。你的真正出路,是把在這裡的每一天,都當成一場偷師學藝。你的目標,不是留下,而是鍍金後,去一片更廣闊的天空!”
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任務,單獨建立一個“服務員”線程池,讓他們去等,別占用我們寶貴的大廚資源。
// 老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。這,是第二個分水嶺。
// 老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%),整個過程技驚四座。那一刻,再也沒有人把他當外包了。
@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,一個只想跟你說點B面真話的師兄。如果這篇文章讓你有了一點點啟發,那就是對我最大的肯定。
為了感謝大家的支持,我把這兩年在一線大廠面試和帶團隊中,沉澱下來的所有上不了台面的私房筆記,整理成了一份《程序員B面生存手冊》。
裡面沒有市面上千篇一律的八股文,只有一些極其管用的“潛規則”和“避坑指南”,希望能幫你少走一些彎路。
關注我的同名公眾號【大廠碼農老A】,在後台回覆“B面”,就能免費獲取。
回覆“簡歷”獲取《簡歷優化手冊》
回覆“arthas”獲取史上最全的《大廠arthas實戰手冊》
回覆“指導”獲取《大廠外包鍍金手冊》
最後,如果覺得內容還行,也希望能點個讚、點個在看,讓更多需要它的兄弟看到。
我們一起,在技術的路上結伴“陪跑”。