大型電商項目異常治理--業(yè)務(wù)異常碼架構(gòu)篇
凌晨兩點的猿媛電商技術(shù)部,應(yīng)急燈還亮著。李建國盯著屏幕上滾動的日志,指尖在鍵盤上敲得飛快 —— 剛上線的 “618 預(yù)售付尾款” 活動,半小時內(nèi)突然涌來 200 多起投訴:用戶付完尾款,訂單卻顯示 “支付失敗”,可銀行賬單明明扣了錢。
日志里的異常碼亂成了一鍋粥:訂單服務(wù)拋 “120302 - 支付結(jié)果校驗失敗”,支付服務(wù)返回 “170103 - 訂單號不存在”,就連之前剛規(guī)范好的營銷服務(wù),也跳出個 “139900 - 通用異?!薄?這串本該歸類到具體模塊的異常碼,竟被開發(fā)圖省事塞進(jìn)了通用區(qū)間。
更糟的是,跨服務(wù)調(diào)用鏈路里,異常碼像斷了線的珠子:支付回調(diào)到訂單服務(wù)時,“170103” 突然變成 “-1”,沒人知道中間哪個環(huán)節(jié)丟了關(guān)鍵信息。李建國揉著發(fā)酸的眼睛,突然意識到:單一服務(wù)的異常碼規(guī)范只是開始,大型電商的復(fù)雜鏈路里,還藏著更難啃的硬骨頭……
一、2018 年:初創(chuàng)期的 “單服務(wù)裸奔”—— 能跑就行,哪管規(guī)范
2018 年夏天,王磊拿著攢下的 20 萬啟動資金,在望京 SOHO 旁邊租了個 10 平米的商住兩居室,拉上剛畢業(yè)的李建國,成立了 “猿媛電商”。當(dāng)時的目標(biāo)很簡單:做校園零食團(tuán)購,讓學(xué)生們在宿舍里點幾下手機,就能買到便宜的薯片、可樂。
“建國,你先搭個能用的系統(tǒng),不用太復(fù)雜,能下單、能收錢就行。” 王磊把一臺二手筆記本電腦推給李建國,“學(xué)生們下周就要開學(xué)了,咱們得趕在開學(xué)前上線?!?/p>
李建國那時候剛從培訓(xùn)班畢業(yè),只會基礎(chǔ)的 Spring Boot 和 MySQL,哪懂什么架構(gòu)設(shè)計。他對著網(wǎng)上的教程,花了三天三夜,搭了個最簡單的單服務(wù)系統(tǒng):
一個com.cxy.eshop包下,塞了所有代碼 ——Controller、Service、Dao 混在一起,連分層都沒有;數(shù)據(jù)庫就一張order表,訂單信息、用戶信息、商品信息全存在里面,字段名起得亂七八糟,user_name和cust_name并存,后來連他自己都分不清哪個是哪個。
異常處理?根本沒有。用戶下單失敗了,就返回 “下單出錯了”,支付出問題了,就返回 “支付失敗”,至于錯在哪,沒人知道?!爱?dāng)時覺得能跑就行,反正訂單量少,學(xué)生們也不挑剔。” 李建國后來回憶起這段日子,總?cè)滩蛔∽猿?,“有次一個學(xué)生下單后沒收到貨,過來問我,我查了半天數(shù)據(jù)庫,才發(fā)現(xiàn)是把‘清華園’寫成了‘清華圓’,地址錯了。”
那時候的單服務(wù),就像個 “五臟俱全的小破屋”—— 雖然簡陋,但勝在靈活。每天訂單量最多幾百單,服務(wù)器用的是阿里云 1 核 2G 的入門實例,跑得還挺順暢。李建國一個人就能搞定所有開發(fā)和運維,白天寫代碼,晚上盯著服務(wù)器日志,偶爾出點小問題,改一行代碼重新部署,就能解決。
王磊負(fù)責(zé)找貨源、談校園代理,每天開著二手面包車去批發(fā)市場進(jìn)貨。兩人分工明確,第一個月就賺了 5 萬塊。慶功宴上,王磊拍著李建國的肩膀說:“建國,以后咱們做大了,就招更多人,你當(dāng)技術(shù)負(fù)責(zé)人!”
李建國那時候還沒意識到,這種 “裸奔” 的單服務(wù),很快就會跟不上業(yè)務(wù)的腳步。
二、2019 年:業(yè)務(wù)擴(kuò)張倒逼 “微服務(wù)拆分”—— 不拆不行,再拖就死了
2019 年初,“猿媛電商” 的業(yè)務(wù)突然爆發(fā)了。王磊談下了北京 5 所高校的校園代理,還把業(yè)務(wù)從校園拓展到了周邊的寫字樓 —— 在寫字樓里放 “智能零食柜”,白領(lǐng)們掃碼就能下單,5 分鐘就能取貨。
訂單量一下從每天幾百單漲到了幾萬單,之前的單服務(wù)瞬間 “扛不住” 了:每天中午 12 點和下午 6 點的下單高峰,服務(wù)器 CPU 使用率直奔 100%,小程序加載半天出不來,用戶在群里罵 “什么破系統(tǒng),比蝸牛還慢”;庫存數(shù)據(jù)經(jīng)常出錯,明明零食柜里還有 10 包薯片,小程序卻顯示 “已售罄”,有次一個白領(lǐng)連續(xù)下單 5 次都失敗,直接投訴到了 12315;最嚴(yán)重的一次,支付回調(diào)延遲了 2 小時,用戶付了錢卻沒收到取貨碼,幾十個人圍著零食柜吵,王磊不得不親自去道歉,免費送了一周的零食,損失了好幾千塊。
“再這么下去,公司就得黃!” 王磊把李建國叫到辦公室,桌子拍得震天響,“必須改系統(tǒng),不能再用單服務(wù)了!”
李建國這才意識到問題的嚴(yán)重性。他連夜查資料,發(fā)現(xiàn)行業(yè)內(nèi)做電商的,大多用 “微服務(wù)架構(gòu)”—— 把原來的單服務(wù)拆成多個小服務(wù),每個服務(wù)負(fù)責(zé)一塊業(yè)務(wù),互相獨立,就算一個服務(wù)出問題,其他服務(wù)也不受影響。
“那咱們就拆!” 李建國咬了咬牙,開始對著行業(yè)架構(gòu)圖,梳理 “猿媛電商” 的業(yè)務(wù)模塊。他發(fā)現(xiàn)核心業(yè)務(wù)可以分成 6 塊:用戶管理、商品管理、訂單管理、支付、庫存、物流。
但真要拆分的時候,才發(fā)現(xiàn)沒那么簡單。原來的單服務(wù)里,代碼耦合得一塌糊涂 ——OrderService里既調(diào)用了支付接口,又操作了庫存數(shù)據(jù),還得處理物流通知。李建國只能一行行讀代碼,把不同業(yè)務(wù)的邏輯拆分開。
“那段時間天天熬夜,眼睛紅得像兔子?!?李建國說,“有次拆訂單和庫存的邏輯,拆到凌晨 3 點,不小心把庫存扣減的代碼刪了,導(dǎo)致第二天線上出現(xiàn)‘超賣’,多賣了 200 包薯片,最后只能給用戶退款道歉?!?/p>
就這樣磕磕絆絆拆了三個月,李建國終于把單服務(wù)拆成了 6 個核心微服務(wù),還加了個網(wǎng)關(guān)服務(wù),用來轉(zhuǎn)發(fā)請求:
- 用戶服務(wù)(user-service):負(fù)責(zé)用戶注冊、登錄、個人信息維護(hù)。之前用戶信息存在order表里,拆出來后建了user表和user_address表,解決了 “一個表存所有信息” 的混亂;
- 商品服務(wù)(product-service):管理商品信息,包括名稱、價格、圖片、保質(zhì)期。之前商品信息和訂單存在一起,拆出來后支持了 “商品上下架” 功能,運營終于不用再找李建國改代碼就能上下架商品;
- 訂單服務(wù)(order-service):核心中的核心,負(fù)責(zé)生成訂單、取消訂單、查詢訂單。李建國把原來的OrderService拆成了OrderCreateService、OrderCancelService、OrderQueryService,職責(zé)更清晰;
- 支付服務(wù)(pay-service):對接微信支付和支付寶,負(fù)責(zé)創(chuàng)建支付單、處理支付回調(diào)。之前支付邏輯嵌在訂單服務(wù)里,拆出來后支持了 “多支付方式”,還加了支付日志,方便排查支付問題;
- 庫存服務(wù)(inventory-service):管理商品庫存,負(fù)責(zé)庫存扣減、庫存查詢。拆出來后加了并發(fā)控制,解決了 “超賣” 問題,這也是李建國最滿意的一個服務(wù);
- 物流服務(wù)(logistics-service):對接第三方物流公司,負(fù)責(zé)生成物流單、通知物流狀態(tài)。之前物流通知靠手動發(fā)短信,拆出來后自動通知,省了不少人力;
- 網(wǎng)關(guān)服務(wù)(api-gateway):統(tǒng)一接收前端請求,轉(zhuǎn)發(fā)到對應(yīng)的微服務(wù),還加了權(quán)限校驗,防止非法請求。
拆分完成后,系統(tǒng)穩(wěn)定性明顯提升了 —— 中午下單高峰,服務(wù)器 CPU 使用率降到了 60%;庫存超賣、支付回調(diào)延遲的問題,也很少出現(xiàn)了。王磊又招了幾個程序員,分別負(fù)責(zé)不同的服務(wù),李建國終于不用一個人扛所有活了。
但好景不長,隨著業(yè)務(wù)繼續(xù)擴(kuò)張,新的問題又出現(xiàn)了。
三、2020-2022 年:微服務(wù) “野蠻生長”—— 服務(wù)越拆越多,異常卻越來越亂
2020 年,“猿媛電商” 又拓展了 “售后” 和 “營銷” 業(yè)務(wù):用戶可以申請售后退款,還能領(lǐng)優(yōu)惠券、參與滿減活動。為了支撐這些新業(yè)務(wù),李建國又新增了 4 個微服務(wù):
- 客服服務(wù)(customer-service):處理用戶售后申請、投訴,客服小姐姐可以在系統(tǒng)里記錄工單;
- 營銷服務(wù)(market-service):負(fù)責(zé)優(yōu)惠券發(fā)放、滿減活動、會員積分,這是提升用戶復(fù)購的關(guān)鍵;
- 履約服務(wù)(fulfill-service):連接訂單和物流,訂單支付后,通知倉庫備貨、發(fā)貨;
- 倉儲服務(wù)(warehouse-service):管理倉庫庫存,負(fù)責(zé)撿貨、打包,之前庫存服務(wù)只管線上庫存,現(xiàn)在線下倉庫也需要專門的服務(wù)。
到 2022 年底,“猿媛電商” 的微服務(wù)數(shù)量已經(jīng)達(dá)到 12 個,加上之前的用戶、商品、訂單等,總共 16 個服務(wù)。服務(wù)間的調(diào)用鏈路也越來越長,比如一個用戶使用優(yōu)惠券下單的完整流程:
- 前端發(fā)起請求,經(jīng)過網(wǎng)關(guān)轉(zhuǎn)發(fā)到訂單服務(wù);
- 訂單服務(wù)調(diào)用用戶服務(wù),驗證用戶是否登錄;
- 調(diào)用商品服務(wù),查詢商品價格和庫存狀態(tài);
- 調(diào)用營銷服務(wù),驗證優(yōu)惠券是否有效、是否可用;
- 調(diào)用庫存服務(wù),鎖定商品庫存(防止超賣);
- 調(diào)用支付服務(wù),創(chuàng)建支付單;
- 用戶支付成功后,支付服務(wù)調(diào)用訂單服務(wù)的回調(diào)接口,更新訂單狀態(tài);
- 訂單服務(wù)調(diào)用履約服務(wù),創(chuàng)建履約單;
- 履約服務(wù)調(diào)用倉儲服務(wù),通知倉庫撿貨;
- 倉儲服務(wù)完成撿貨后,調(diào)用物流服務(wù),生成物流單,通知快遞公司取貨。
這么長的鏈路,只要有一個服務(wù)拋出異常,整個流程就會卡住。更要命的是,每個服務(wù)的異常處理都 “各自為戰(zhàn)”—— 沒有統(tǒng)一的規(guī)范,每個程序員都按自己的習(xí)慣拋異常:
- 用戶服務(wù)的 “用戶不存在”,返回 “-1”;
- 商品服務(wù)的 “商品已下架”,返回 “goods_off_shelf”;
- 營銷服務(wù)的 “優(yōu)惠券已過期”,返回 “coupon_expired_1001”;
- 訂單服務(wù)更亂,有時返回數(shù)字,有時返回字符串,遇到?jīng)]處理的異常,直接拋 RuntimeException,前端收到的就是 “500 Internal Server Error”。
“那時候前端開發(fā)小美,每天都要拿著錯誤信息跑來問我:‘建國哥,這個 “-1” 是啥意思?。窟@個 “coupon_expired_1001” 是哪個服務(wù)拋的???’” 李建國苦笑著說,“我也答不上來,只能讓她去對應(yīng)的服務(wù)日志里找,有時候查半天都找不到問題根源?!?/p>
2022 年雙 11,線上出了個大故障:有用戶反映,用優(yōu)惠券下單后,支付成功了,卻一直顯示 “待支付”,優(yōu)惠券也被鎖定了,不能用也不能退。
李建國帶著團(tuán)隊排查了整整 6 個小時,才找到問題所在:
- 用戶下單時,營銷服務(wù)鎖定優(yōu)惠券成功,但返回異常碼時,程序員手滑把 “200 - 鎖定成功” 寫成了 “201 - 鎖定失敗”;
- 訂單服務(wù)收到 “201” 異常碼,以為優(yōu)惠券鎖定失敗,就沒更新訂單狀態(tài),但實際上庫存已經(jīng)扣減了,優(yōu)惠券也鎖定了;
- 用戶支付成功后,支付服務(wù)調(diào)用訂單服務(wù)回調(diào)接口,訂單服務(wù)因為訂單狀態(tài)是 “待支付”,拒絕處理,返回 “order_status_error”;
- 支付服務(wù)收到這個異常,不知道該怎么處理,就拋了個 RuntimeException,導(dǎo)致支付狀態(tài)沒同步到訂單服務(wù)。
最后,李建國只能手動修改數(shù)據(jù)庫,給用戶解鎖優(yōu)惠券、更新訂單狀態(tài),忙到凌晨 3 點才搞定。第二天,老板王磊把全部門的人叫到辦公室,發(fā)了大火:“咱們現(xiàn)在每天訂單量幾十萬,還這么搞異常處理,遲早要出大問題!必須定個規(guī)范,把這些異常管起來!”
也就是從那天起,李建國意識到:微服務(wù)光拆分還不夠,必須做 “異常治理”,尤其是統(tǒng)一的錯誤碼規(guī)范 —— 不然服務(wù)越多,亂得越厲害,最后整個系統(tǒng)都會變成 “一團(tuán)亂麻”。
四、2023 年:異常治理的 “破局”—— 從錯誤碼規(guī)范開始,給異常 “立規(guī)矩”
2023 年初,“猿媛電商” 年交易額突破 10 億元,王磊把李建國提拔為技術(shù)經(jīng)理,讓他牽頭做 “異常治理”。李建國做的第一件事,就是梳理所有服務(wù)的異常場景,制定統(tǒng)一的錯誤碼規(guī)范。
“那時候我?guī)е鴥蓚€程序員,花了一個月時間,把 16 個服務(wù)的所有異常都過了一遍?!?李建國說,“每個服務(wù)有多少個異常場景,每個場景該返回什么錯誤碼,都記在本子上,最后整理出了一個‘錯誤碼規(guī)范文檔’?!?/p>
這個規(guī)范的核心,是 “6 位數(shù)字錯誤碼”,分三段式結(jié)構(gòu):
- 前兩位(服務(wù)標(biāo)識):代表異常所屬的微服務(wù),比如用戶服務(wù)是 10,商品服務(wù)是 11,訂單服務(wù)是 12,營銷服務(wù)是 13,這樣一看前兩位,就知道異常來自哪個服務(wù); 系統(tǒng)級別錯誤:
圖片
業(yè)務(wù)級別錯誤:
圖片
- 中間兩位(模塊標(biāo)識):代表服務(wù)內(nèi)的具體模塊,比如訂單服務(wù)的 “創(chuàng)建訂單” 是 01,“取消訂單” 是 02,“查詢訂單” 是 03,這樣能快速定位到具體業(yè)務(wù)模塊;
圖片
- 最后兩位(異常序號):代表該模塊下的具體異常,從 00 開始遞增,比如訂單服務(wù) “創(chuàng)建訂單” 模塊下,“訂單已存在” 是 00,“商品不存在” 是 01,“優(yōu)惠券不可用” 是 02。
比如:
- 120100:12(訂單服務(wù))+01(創(chuàng)建訂單)+00(訂單已存在)→ 訂單已存在;
- 130201:13(營銷服務(wù))+02(優(yōu)惠券鎖定)+01(優(yōu)惠券已過期)→ 優(yōu)惠券已過期;
- 110302:11(商品服務(wù))+03(商品查詢)+02(商品已下架)→ 商品已下架。為了讓規(guī)范落地,李建國還做了三件事:
- 開發(fā)統(tǒng)一的異常枚舉包:在cxy-eshop-common工程里,為每個服務(wù)創(chuàng)建對應(yīng)的異常枚舉類,比如UserErrorCodeEnum、ProductErrorCodeEnum、OrderErrorCodeEnum,每個枚舉值都按 “6 位錯誤碼 + 錯誤信息” 定義,還加了詳細(xì)注釋,說明異常場景和處理建議;
// 訂單服務(wù)異常枚舉示例
public enum OrderErrorCodeEnum {
/**
* 創(chuàng)建訂單 - 訂單已存在
* 場景:用戶重復(fù)提交訂單(如連續(xù)點擊下單按鈕)
* 處理建議:前端做按鈕置灰,后端加用戶ID+商品ID冪等校驗
*/
ORDER_EXISTED("120100", "訂單已存在,請勿重復(fù)提交"),
/**
* 創(chuàng)建訂單 - 商品不存在
* 場景:用戶下單時,商品已被刪除或下架
* 處理建議:前端提示用戶“商品已下架”,引導(dǎo)用戶返回商品列表
*/
PRODUCT_NOT_EXIST("120101", "商品不存在或已下架"),
// 其他異常...
}- 做全公司培訓(xùn):李建國組織了 3 場培訓(xùn),給所有開發(fā)、測試、前端講錯誤碼規(guī)范,還搞了個 “錯誤碼默寫大賽”,默寫全對的獎勵零食大禮包,錯一個的罰抄 10 遍規(guī)范文檔?!澳菚r候連客服小姐姐都知道,看到 13 開頭的異常,就找營銷服務(wù)的開發(fā),效率提升了不少?!?/li>
- 加代碼審查:在代碼提交前,必須檢查異常處理是否符合規(guī)范,錯誤碼是否正確,不符合的一律打回。有次新員工小陳把訂單服務(wù)的 “取消訂單” 異常碼寫成了 120300(正確是 120200),被李建國打回,罰他抄了 5 遍規(guī)范文檔,從此再也沒人敢隨便寫錯誤碼。
五、異常治理前的 “異常迷宮”——try-catch 堆成山,排查故障靠 “猜”
錯誤碼規(guī)范落地后,李建國以為異常治理能順利推進(jìn),可沒過多久就發(fā)現(xiàn):新的問題又來了 —— 服務(wù)間調(diào)用的異常處理還是一團(tuán)糟,尤其是 RPC 服務(wù)端和 Web 層,重復(fù)的 try-catch 代碼堆得像座小山。
那天李建國翻營銷服務(wù)的代碼,看到MarketRemoteImpl里的lockUserCoupon方法,氣得差點把鍵盤摔了:
@Override
public JsonResult<Boolean> lockUserCoupon(LockUserCouponRequest request) {
try {
log.info("lockUserCoupon request:{}", JSON.toJSONString(request));
// 調(diào)用優(yōu)惠券服務(wù)鎖定優(yōu)惠券
Boolean result = couponService.lockUserCoupon(request);
log.info("lockUserCoupon response:{}", result);
return JsonResult.buildSuccess(result);
} catch (MarketBizException e) {
// 捕獲營銷業(yè)務(wù)異常
log.error("lockUserCoupon biz error, request:{}", JSON.toJSONString(request), e);
return JsonResult.buildError(e.getErrorCode(), e.getErrorMsg());
} catch (DubboException e) {
// 捕獲Dubbo調(diào)用異常
log.error("lockUserCoupon dubbo error, request:{}", JSON.toJSONString(request), e);
return JsonResult.buildError("999999", "服務(wù)調(diào)用失敗,請稍后再試");
} catch (Exception e) {
// 捕獲系統(tǒng)未知異常
log.error("lockUserCoupon system error, request:{}", JSON.toJSONString(request), e);
return JsonResult.buildError(CommonErrorCodeEnum.SYSTEM_UNKNOWN_ERROR.getErrorCode(),
CommonErrorCodeEnum.SYSTEM_UNKNOWN_ERROR.getErrorMsg());
}
}“全公司 16 個服務(wù),每個服務(wù)平均 10 個 RPC 接口,每個接口都這么寫 try-catch,改個日志格式都要改 160 處!” 李建國把開發(fā)們叫到會議室,把這段代碼投在屏幕上,“上次小陳改營銷服務(wù)的異常返回格式,漏了 3 個接口,導(dǎo)致線上出現(xiàn)‘999999’和‘-1’兩種系統(tǒng)錯誤碼,用戶投訴說‘你們系統(tǒng)怎么一會兒一個錯?’”
更頭疼的是 RPC 調(diào)用的異常流程。李建國畫了張流程圖,貼在會議室墻上:
- 訂單服務(wù)(Dubbo 消費者)調(diào)用營銷服務(wù)(Dubbo 提供者)的lockUserCoupon接口;
- 營銷服務(wù)拋出MarketBizException(錯誤碼 130201,優(yōu)惠券已過期);
- Dubbo 原生的ExceptionFilter把MarketBizException包裝成RuntimeException,還丟了錯誤碼;
- 訂單服務(wù)消費者收到RuntimeException,進(jìn)入自己的catch (Exception e)塊;
- 訂單服務(wù)返回 “-1 - 系統(tǒng)未知異?!?給 Web 層;
- Web 層的 Controller 又套了一層 try-catch,最后返回給前端的還是 “系統(tǒng)未知異?!薄?/li>
“用戶明明是優(yōu)惠券過期,卻看到‘系統(tǒng)未知異?!?,能不罵娘嗎?” 測試林曉拿著測試報告補充,“我上周測了 20 個異常場景,有 15 個最后都顯示‘系統(tǒng)未知異?!?,根本沒法驗證異常處理是否正確。”
Web 層的問題也不小。訂單服務(wù)的OrderController里,每個接口都裹著 try-catch:
@PostMapping("/createOrder")
public JsonResult<CreateOrderDTO> createOrder(@RequestBody CreateOrderRequest request) {
try {
log.info("createOrder request:{}", JSON.toJSONString(request));
CreateOrderDTO result = orderService.createOrder(request);
return JsonResult.buildSuccess(result);
} catch (OrderBizException e) {
log.error("createOrder biz error, request:{}", JSON.toJSONString(request), e);
return JsonResult.buildError(e.getErrorCode(), e.getErrorMsg());
} catch (Exception e) {
log.error("createOrder system error, request:{}", JSON.toJSONString(request), e);
return JsonResult.buildError("-1", "系統(tǒng)繁忙,請稍后再試");
}
}“前端小美跟我吐槽,同一個‘訂單已存在’異常,在下單接口返回 120100,在取消訂單接口卻返回‘系統(tǒng)繁忙’,她都不知道該怎么統(tǒng)一提示用戶?!?李建國揉了揉太陽穴,“必須徹底重構(gòu)異常處理流程,把這些重復(fù)的 try-catch 全干掉!”
六、RPC 服務(wù)端異常治理:Dubbo 過濾器 “救場”—— 干掉 try-catch,異常透傳不打折
李建國把 RPC 服務(wù)端治理的核心定為 “用 Dubbo 過濾器統(tǒng)一處理異常”。他翻了三天 Dubbo 官方文檔,終于理清了思路:重寫原生ExceptionFilter解決異常包裝問題,再開發(fā)自定義過濾器統(tǒng)一日志和異常返回,兩步走搞定 RPC 異常。
圖片
第一步:定義統(tǒng)一業(yè)務(wù)異常 —— 給異常 “定家譜”
要讓過濾器識別業(yè)務(wù)異常,首先得有統(tǒng)一的異常父類。李建國在cxy-eshop-common工程里新建了BaseBizException,所有業(yè)務(wù)異常都繼承它:
package com.cxy.eshop.common.exception;
import lombok.Getter;
/**
* 業(yè)務(wù)異常父類,所有業(yè)務(wù)異常必須繼承此類
* 用于Dubbo過濾器識別業(yè)務(wù)異常,避免被包裝成RuntimeException
*/
@Getter
public class BaseBizException extends RuntimeException {
/**
* 錯誤碼(6位數(shù)字,遵循錯誤碼規(guī)范)
*/
private final String errorCode;
/**
* 錯誤信息
*/
private final String errorMsg;
public BaseBizException(String errorCode, String errorMsg) {
super(errorMsg);
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
public BaseBizException(ErrorCode errorCodeEnum) {
super(errorCodeEnum.getErrorMsg());
this.errorCode = errorCodeEnum.getErrorCode();
this.errorMsg = errorCodeEnum.getErrorMsg();
}
}然后讓各個服務(wù)的業(yè)務(wù)異常繼承它,比如營銷服務(wù)的MarketBizException:
package com.cxy.eshop.market.exception;
import com.cxy.eshop.common.exception.BaseBizException;
import com.cxy.eshop.common.exception.ErrorCode;
/**
* 營銷服務(wù)業(yè)務(wù)異常
*/
public class MarketBizException extends BaseBizException {
public MarketBizException(String errorCode, String errorMsg) {
super(errorCode, errorMsg);
}
public MarketBizException(ErrorCode errorCodeEnum) {
super(errorCodeEnum);
}
}“這樣一來,所有業(yè)務(wù)異常都有了‘家譜’,Dubbo 過濾器只要判斷異常是不是 BaseBizException 的子類,就能識別業(yè)務(wù)異常了?!?李建國在技術(shù)分享會上解釋。
第二步:重寫 Dubbo 原生 ExceptionFilter—— 阻止異常 “被包裝”
Dubbo 原生的ExceptionFilter會把自定義異常包裝成RuntimeException,李建國要做的就是 “截胡”—— 在包裝前把業(yè)務(wù)異常拎出來,直接返回錯誤碼和信息。
他在cxy-eshop-common里新建DubboExceptionFilter,繼承原生過濾器,重寫invoke方法:
package com.cxy.eshop.common.dubbo;
import com.cxy.eshop.common.exception.BaseBizException;
import com.cxy.eshop.common.exception.JsonResult;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
import org.apache.dubbo.rpc.filter.ExceptionFilter;
import org.apache.dubbo.rpc.service.GenericService;
import java.lang.reflect.Method;
/**
* 重寫Dubbo原生ExceptionFilter,解決業(yè)務(wù)異常被包裝問題
*/
@Activate(group = CommonConstants.PROVIDER)
public class DubboExceptionFilter extends ExceptionFilter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
Result result = invoker.invoke(invocation);
// 只處理有異常且非泛化調(diào)用的情況
if (result.hasException() && GenericService.class != invoker.getInterface()) {
try {
Throwable exception = result.getException();
// 關(guān)鍵:如果是業(yè)務(wù)異常,直接返回錯誤碼和信息,不包裝
if (exception instanceof BaseBizException) {
BaseBizException bizException = (BaseBizException) exception;
JsonResult<Object> errorResult = JsonResult.buildError(
bizException.getErrorCode(),
bizException.getErrorMsg()
);
// 用AsyncRpcResult包裝,避免Dubbo二次處理
return new AsyncRpcResult(ResultType.NORMAL_VALUE, errorResult, invocation);
}
// 非業(yè)務(wù)異常,走原生邏輯(比如檢查是否在方法聲明的異常列表中)
Method method = invoker.getInterface().getMethod(
invocation.getMethodName(),
invocation.getParameterTypes()
);
Class<?>[] exceptionClasses = method.getExceptionTypes();
for (Class<?> exceptionClass : exceptionClasses) {
if (exception.getClass().equals(exceptionClass)) {
return result;
}
}
// 未聲明的非業(yè)務(wù)異常,包裝成系統(tǒng)錯誤
JsonResult<Object> systemError = JsonResult.buildError(
"-1", "系統(tǒng)未知異常,請聯(lián)系管理員"
);
return new AsyncRpcResult(ResultType.NORMAL_VALUE, systemError, invocation);
} catch (Throwable e) {
// 防止過濾器自身出錯
return result;
}
}
return result;
}
}要讓這個過濾器生效,還得在cxy-eshop-common的resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter里加配置,用自定義過濾器覆蓋原生的:
# 覆蓋Dubbo原生ExceptionFilter,名稱必須是exception
exception=com.cxy.eshop.common.dubbo.DubboExceptionFilter“這里踩過坑!” 李建國在文檔里加了個紅色警告,“一開始用了別的名稱,結(jié)果原生過濾器還在生效,業(yè)務(wù)異常還是被包裝,后來查文檔才知道,必須用‘exception’這個名稱才能覆蓋?!?/p>
第三步:新增 CustomerExceptionFilter—— 統(tǒng)一日志和耗時統(tǒng)計
解決了異常包裝問題,李建國又開發(fā)了CustomerExceptionFilter,把日志打印、耗時統(tǒng)計、異常返回全統(tǒng)一了:
package com.cxy.eshop.common.dubbo;
import com.alibaba.fastjson.JSON;
import com.cxy.eshop.common.exception.BaseBizException;
import com.cxy.eshop.common.exception.CommonErrorCodeEnum;
import com.cxy.eshop.common.exception.JsonResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
/**
* 自定義Dubbo過濾器:統(tǒng)一日志、耗時統(tǒng)計、異常處理
*/
@Slf4j
@Activate(group = CommonConstants.PROVIDER, order = 100) // order越大,執(zhí)行越靠后
public class CustomerExceptionFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
long startTime = System.currentTimeMillis();
String serviceName = invoker.getInterface().getName();
String methodName = invocation.getMethodName();
String paramJson = JSON.toJSONString(invocation.getArguments());
log.info("[Dubbo調(diào)用開始] service:{}, method:{}, param:{}",
serviceName, methodName, paramJson);
try {
// 調(diào)用目標(biāo)方法
Result result = invoker.invoke(invocation);
long costTime = System.currentTimeMillis() - startTime;
if (result.hasException()) {
// 處理異常(此時業(yè)務(wù)異常已被DubboExceptionFilter處理,這里只處理系統(tǒng)異常)
Throwable e = result.getException();
log.error("[Dubbo調(diào)用異常] service:{}, method:{}, param:{}, costTime:{}ms",
serviceName, methodName, paramJson, costTime, e);
JsonResult<Object> errorResult = JsonResult.buildError(
CommonErrorCodeEnum.SYSTEM_UNKNOWN_ERROR.getErrorCode(),
CommonErrorCodeEnum.SYSTEM_UNKNOWN_ERROR.getErrorMsg()
);
result.setValue(errorResult);
result.setException(null); // 屏蔽原始異常,避免泄露敏感信息
} else {
log.info("[Dubbo調(diào)用成功] service:{}, method:{}, costTime:{}ms, result:{}",
serviceName, methodName, costTime, JSON.toJSONString(result.getValue()));
}
return result;
} catch (BaseBizException e) {
// 捕獲方法內(nèi)部主動拋出的業(yè)務(wù)異常
long costTime = System.currentTimeMillis() - startTime;
log.error("[Dubbo業(yè)務(wù)異常] service:{}, method:{}, param:{}, costTime:{}ms, errorCode:{}, errorMsg:{}",
serviceName, methodName, paramJson, costTime, e.getErrorCode(), e.getErrorMsg(), e);
JsonResult<Object> errorResult = JsonResult.buildError(e.getErrorCode(), e.getErrorMsg());
return new AsyncRpcResult(ResultType.NORMAL_VALUE, errorResult, invocation);
} catch (Exception e) {
// 捕獲系統(tǒng)異常
long costTime = System.currentTimeMillis() - startTime;
log.error("[Dubbo系統(tǒng)異常] service:{}, method:{}, param:{}, costTime:{}ms",
serviceName, methodName, paramJson, costTime, e);
JsonResult<Object> errorResult = JsonResult.buildError(
CommonErrorCodeEnum.SYSTEM_UNKNOWN_ERROR.getErrorCode(),
CommonErrorCodeEnum.SYSTEM_UNKNOWN_ERROR.getErrorMsg()
);
return new AsyncRpcResult(ResultType.NORMAL_VALUE, errorResult, invocation);
}
}
}配置這個過濾器也得兩步:
- 在META-INF/dubbo/org.apache.dubbo.rpc.Filter里注冊:
customerExceptionFilter=com.cxy.eshop.common.dubbo.CustomerExceptionFilter- 在每個服務(wù)的application.yml里啟用:
dubbo:
provider:
filter: customerExceptionFilter # 啟用自定義過濾器
timeout: 3000第四步:消除 Dubbo 服務(wù)端模板代碼 —— 代碼清爽得像 “剛洗澡”
過濾器部署完成后,李建國帶頭改造營銷服務(wù)的MarketRemoteImpl。之前 18 行的lockUserCoupon方法,現(xiàn)在只剩 3 行:
@Override
public JsonResult<Boolean> lockUserCoupon(LockUserCouponRequest request) {
// 直接調(diào)用業(yè)務(wù)方法,不用try-catch,過濾器會處理異常
Boolean result = couponService.lockUserCoupon(request);
return JsonResult.buildSuccess(result);
}“太爽了!” 負(fù)責(zé)營銷服務(wù)的小陳改完代碼,興奮地跑到李建國工位,“以前改個業(yè)務(wù)邏輯,還得小心翼翼別碰壞 try-catch,現(xiàn)在直接寫核心代碼,效率至少提升一倍!”
李建國還特意做了個對比測試:在營銷服務(wù)拋出 “130201 - 優(yōu)惠券已過期” 異常,訂單服務(wù)調(diào)用后,直接拿到了包含錯誤碼的JsonResult,再也沒有被包裝成RuntimeException。前端收到異常后,準(zhǔn)確顯示 “優(yōu)惠券已過期,請更換優(yōu)惠券”,用戶投訴量一下降了 40%。
七、 Web 層異常治理:全局?jǐn)r截器 “收尾”——Controller 告別 try-catch
RPC 服務(wù)端搞定了,Web 層的問題還沒解決。李建國看著訂單服務(wù)OrderController里重復(fù)的 try-catch,決定用@RestControllerAdvice + @ExceptionHandler做全局異常攔截。
第一步:開發(fā)全局異常攔截器 ——Web 層的 “異常保安”
他在cxy-eshop-common里新建GlobalExceptionHandler,統(tǒng)一處理 Web 層所有異常:
package com.cxy.eshop.common.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.bind.MethodArgumentNotValidException;
import javax.servlet.http.HttpServletRequest;
import java.util.stream.Collectors;
/**
* Web層全局異常攔截器,所有Controller的異常都會被這里處理
* Order設(shè)置為最高優(yōu)先級,確保先于其他攔截器執(zhí)行
*/
@Slf4j
@RestControllerAdvice // 對所有@RestController生效,自動返回JSON格式
@Order(Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler {
/**
* 處理業(yè)務(wù)異常(BaseBizException及其子類)
* 比如訂單服務(wù)的OrderBizException、營銷服務(wù)的MarketBizException
*/
@ExceptionHandler(value = BaseBizException.class)
public JsonResult<Object> handleBizException(BaseBizException e, HttpServletRequest request) {
String requestUrl = request.getRequestURI();
String method = request.getMethod();
// 打印業(yè)務(wù)異常日志,包含請求地址、請求方法,方便排查
log.error("[Web業(yè)務(wù)異常] url:{}, method:{}, errorCode:{}, errorMsg:{}",
requestUrl, method, e.getErrorCode(), e.getErrorMsg(), e);
// 直接返回業(yè)務(wù)異常的錯誤碼和信息,符合前端預(yù)期格式
return JsonResult.buildError(e.getErrorCode(), e.getErrorMsg());
}
/**
* 處理請求參數(shù)校驗異常(比如@NotNull、@NotBlank注解觸發(fā)的異常)
* 之前這類異常需要在Controller里手動捕獲,現(xiàn)在統(tǒng)一處理
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public JsonResult<Object> handleParamValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
String requestUrl = request.getRequestURI();
String method = request.getMethod();
// 提取參數(shù)校驗失敗的信息(比如“訂單號不能為空”)
String errorMsg = e.getBindingResult().getFieldErrors().stream()
.map(fieldError -> fieldError.getField() + ":" + fieldError.getDefaultMessage())
.collect(Collectors.joining("; "));
// 打印參數(shù)校驗日志
log.error("[Web參數(shù)校驗異常] url:{}, method:{}, errorMsg:{}",
requestUrl, method, errorMsg, e);
// 返回客戶端通用參數(shù)錯誤碼1002
return JsonResult.buildError(CommonErrorCodeEnum.CLIENT_REQUEST_BODY_VALID_ERROR);
}
/**
* 處理系統(tǒng)未知異常(所有未捕獲的異常都會走到這里)
*/
@ExceptionHandler(value = Exception.class)
public JsonResult<Object> handleSystemException(Exception e, HttpServletRequest request) {
String requestUrl = request.getRequestURI();
String method = request.getMethod();
// 打印系統(tǒng)異常日志,包含堆棧信息,方便定位問題
log.error("[Web系統(tǒng)未知異常] url:{}, method:{}, errorMsg:{}",
requestUrl, method, e.getMessage(), e);
// 返回系統(tǒng)未知異常碼-1,避免暴露敏感信息
return JsonResult.buildError(CommonErrorCodeEnum.SYSTEM_UNKNOWN_ERROR);
}
}寫完代碼,李建國特意加了三個關(guān)鍵處理邏輯:
- 業(yè)務(wù)異常精準(zhǔn)返回:直接提取BaseBizException的錯誤碼和信息,保證前端拿到的異常格式統(tǒng)一;
- 參數(shù)校驗異常自動處理:之前需要在 Controller 里寫@Valid + BindingResult手動判斷,現(xiàn)在攔截器自動收集校驗失敗信息,返回 1002 客戶端參數(shù)錯誤碼;
- 系統(tǒng)異常脫敏:只返回 “系統(tǒng)未知異?!?提示,不暴露堆棧信息,避免黑客利用漏洞。
“以前參數(shù)校驗要寫這么多代碼,” 李建國翻出之前的 Controller 代碼給同事看,“現(xiàn)在一行都不用寫,攔截器全搞定!”
第二步:改造 Controller—— 代碼 “瘦身”,告別 try-catch
全局?jǐn)r截器部署到cxy-eshop-common后,李建國帶頭改造訂單服務(wù)的OrderController。之前 15 行的createOrder接口,現(xiàn)在只剩 5 行:
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 提交訂單接口
* 改造后:無try-catch,無日志打印,無參數(shù)校驗判斷
*/
@PostMapping("/createOrder")
public JsonResult<CreateOrderDTO> createOrder(
// @Valid觸發(fā)參數(shù)校驗,攔截器會處理校驗失敗異常
@Valid @RequestBody CreateOrderRequest request) {
// 只保留核心業(yè)務(wù)邏輯,異常全靠GlobalExceptionHandler處理
CreateOrderDTO result = orderService.createOrder(request);
return JsonResult.buildSuccess(result);
}
/**
* 取消訂單接口
* 之前因try-catch漏寫,導(dǎo)致返回“系統(tǒng)繁忙”,現(xiàn)在不會再出現(xiàn)
*/
@PostMapping("/cancelOrder")
public JsonResult<Boolean> cancelOrder(
@RequestParam("orderId") String orderId,
@RequestParam("userId") String userId) {
Boolean result = orderService.cancelOrder(orderId, userId);
return JsonResult.buildSuccess(result);
}
}負(fù)責(zé)訂單服務(wù)的開發(fā)老周改完代碼,興奮地拍了拍李建國的肩膀:“建國,你這攔截器太牛了!以前改個訂單狀態(tài)邏輯,還得小心翼翼別碰錯 try-catch,現(xiàn)在直接寫業(yè)務(wù)代碼,效率至少提了三成!”
更讓前端小美開心的是,異常格式終于統(tǒng)一了。之前同一個 “訂單已存在” 異常,在下單接口返回{"errorCode":"120100","errorMsg":"訂單已存在"},在取消訂單接口返回{"errorCode":"-1","errorMsg":"系統(tǒng)繁忙"},現(xiàn)在不管哪個接口拋出OrderBizException,都返回統(tǒng)一格式,她再也不用寫一堆 “if-else” 判斷錯誤碼了。
“現(xiàn)在我只要根據(jù) errorCode 就能寫提示,12 開頭就是訂單服務(wù)的問題,13 開頭就是營銷服務(wù),太省心了!” 小美特意跑來給李建國送了杯奶茶,“之前處理異常要寫 50 行代碼,現(xiàn)在 10 行就搞定!”
第三步:落地踩坑與優(yōu)化 —— 細(xì)節(jié)決定成敗
但全局?jǐn)r截器落地時,還是出了小插曲。測試林曉在測 “查詢訂單詳情” 接口時,發(fā)現(xiàn)傳入非法的訂單號(比如 “abc123”),攔截器返回的是 “系統(tǒng)未知異?!?,而不是預(yù)期的 “訂單號格式錯誤”。
“建國哥,這不對?。 ?林曉拿著測試報告找到李建國,“訂單號格式錯誤應(yīng)該是客戶端參數(shù)錯誤,返回 1004 才對,怎么返回 - 1 了?”
李建國查了日志才發(fā)現(xiàn),訂單服務(wù)的getOrderDetail方法里,把 “訂單號格式錯誤” 拋成了IllegalArgumentException,而不是自定義的OrderBizException,導(dǎo)致攔截器把它當(dāng)成系統(tǒng)異常處理,返回了 - 1。
“看來光有攔截器還不夠,得規(guī)范異常拋出!” 李建國立刻組織開發(fā)們開了個短會,強調(diào)兩條規(guī)則:
- 業(yè)務(wù)相關(guān)異常必須拋自定義業(yè)務(wù)異常:比如參數(shù)格式錯誤、業(yè)務(wù)邏輯不滿足(如訂單已支付不能取消),必須拋對應(yīng)服務(wù)的 BizException,指定明確錯誤碼;
- 非業(yè)務(wù)異常(如空指針)要提前預(yù)防:通過參數(shù)校驗、判空等方式避免,實在無法避免的,在最外層 Service 拋BaseBizException(指定通用錯誤碼),不讓它走到系統(tǒng)異常攔截邏輯。
會后,老周把getOrderDetail里的IllegalArgumentException改成了OrderBizException:
public OrderDetailDTO getOrderDetail(String orderId) {
// 訂單號格式校驗:如果不是數(shù)字,拋業(yè)務(wù)異常
if (!orderId.matches("\\d+")) {
throw new OrderBizException(OrderErrorCodeEnum.ORDER_ID_FORMAT_ERROR);
// OrderErrorCodeEnum.ORDER_ID_FORMAT_ERROR的錯誤碼是120601,含義“訂單號格式錯誤”
}
// 后續(xù)業(yè)務(wù)邏輯...
}再次測試,傳入 “abc123” 的訂單號,接口正確返回{"errorCode":"120601","errorMsg":"訂單號格式錯誤,僅支持?jǐn)?shù)字"},林曉這才滿意地在測試報告上打了 “通過”。
還有個坑是 “攔截器優(yōu)先級”??头?wù)的開發(fā)小張自己寫了個CustomerExceptionHandler,優(yōu)先級比全局?jǐn)r截器還高,導(dǎo)致客服服務(wù)的業(yè)務(wù)異常被小張的攔截器處理,返回了非標(biāo)準(zhǔn)格式的錯誤信息。
李建國查了代碼,發(fā)現(xiàn)小張的攔截器沒加@Order注解,默認(rèn)優(yōu)先級比全局?jǐn)r截器低,可小張在@RestControllerAdvice里指定了basePackages = "com.cxy.eshop.customer",只處理客服服務(wù)的 Controller,反而覆蓋了全局?jǐn)r截器。
“解決辦法很簡單,在全局?jǐn)r截器的@RestControllerAdvice里也加 basePackages,并且把優(yōu)先級設(shè)為最高?!?李建國幫小張修改了全局?jǐn)r截器的注解:
// 只處理公司內(nèi)部服務(wù)的Controller,避免影響第三方依賴
@RestControllerAdvice(basePackages = "com.cxy.eshop")
@Order(Ordered.HIGHEST_PRECEDENCE) // 最高優(yōu)先級,確保先執(zhí)行
public class GlobalExceptionHandler {
// ...
}這樣一來,全局?jǐn)r截器會優(yōu)先處理所有com.cxy.eshop包下的 Controller 異常,小張的客服服務(wù)攔截器只作為補充,不會覆蓋全局規(guī)則。
八、異常治理落地效果 —— 從 “混亂” 到 “有序” 的蛻變
經(jīng)過三個月的攻堅,猿媛電商的異常治理終于全面落地。李建國做了個數(shù)據(jù)統(tǒng)計,效果讓全公司都驚訝:
- 代碼冗余減少 60%:全公司 16 個服務(wù),共刪除重復(fù) try-catch 代碼約 8000 行,平均每個接口代碼量減少 50%;
- 故障排查時間縮短 80%:之前排查一個異常平均需要 2 小時,現(xiàn)在看錯誤碼就能定位到服務(wù)和模塊,平均 20 分鐘就能解決;
- 用戶投訴量下降 75%:因 “系統(tǒng)未知異常” 導(dǎo)致的投訴從每月 120 起,降到了每月 30 起以下;
- 開發(fā)效率提升 40%:新接口開發(fā)時間從平均 2 天,縮短到 1.2 天,不用再花時間寫重復(fù)的異常處理代碼。
2023 年底的技術(shù)總結(jié)會上,李建國展示了治理前后的對比:
- 治理前:用戶下單用優(yōu)惠券,因營銷服務(wù)異常,返回 “系統(tǒng)繁忙”,用戶不知道是優(yōu)惠券過期;
- 治理后:相同場景下,接口返回 “130201 - 優(yōu)惠券已過期,請更換優(yōu)惠券”,用戶直接換券下單,轉(zhuǎn)化率提升了 15%。
老板王磊拿著這份數(shù)據(jù),在全公司大會上表揚了技術(shù)部:“以前用戶總說咱們系統(tǒng)‘不穩(wěn)定’,現(xiàn)在很少聽到這種抱怨了。異常治理不僅提升了用戶體驗,還幫公司省了不少售后成本,李建國這個技術(shù)經(jīng)理,沒白提!”
2023 年底的公司年會上,王磊把 “年度技術(shù)貢獻(xiàn)獎” 頒給了李建國,獎金 10 萬元。“建國,你搞的異常治理,比加 10 臺服務(wù)器還管用!” 王磊拍著他的肩膀說,“明年咱們要把業(yè)務(wù)拓展到全國,你繼續(xù)牽頭做架構(gòu)優(yōu)化,爭取成為真正的架構(gòu)師!”
李建國拿著獎杯,看著臺下的張萌(此時已經(jīng)是他的未婚妻),眼眶有點濕潤。他想起 2018 年剛?cè)肼殨r,那個連單服務(wù)都寫不規(guī)范的自己;想起 2019 年拆分微服務(wù)時,熬夜改 bug 的日子;想起 2022 年雙 11 排查故障時的焦慮。
“從單服務(wù)到微服務(wù),再到異常治理,其實就是公司成長的縮影?!?李建國在獲獎感言里說,“技術(shù)從來不是孤立的,而是跟著業(yè)務(wù)走的 —— 業(yè)務(wù)需要什么,我們就做什么;哪里有問題,我們就解決哪里。這就是我們程序員的價值。”
臺下響起了熱烈的掌聲,張萌笑著給他比了個 “加油” 的手勢。李建國知道,這只是開始,未來還有更多的技術(shù)挑戰(zhàn)等著他,但他已經(jīng)做好了準(zhǔn)備 —— 跟著 “猿媛電商” 一起,繼續(xù)成長,繼續(xù)破局。
臺下的李建國看著身邊的張萌(此時已經(jīng)是他的妻子),心里滿是感慨。他想起 2018 年剛?cè)肼殨r,那個連單服務(wù)異常都處理不好的自己;想起 2019 年拆分微服務(wù)時,熬夜改 bug 的焦慮;想起 2023 年推進(jìn)異常治理時,遇到的各種阻力。
“異常治理不是終點,而是新的起點?!?李建國在年會的最后說,“接下來我們還要做異常監(jiān)控平臺,把所有異常碼匯總起來,實時預(yù)警;還要做異常溯源,讓每個異常都能查到完整的調(diào)用鏈路。技術(shù)永遠(yuǎn)在進(jìn)步,我們也得跟著進(jìn)步,才能跟上公司發(fā)展的腳步?!?/p>
散會后,張萌走過來,悄悄遞給李建國一個保溫杯:“別總熬夜了,現(xiàn)在系統(tǒng)穩(wěn)定了,也該多陪陪我和孩子了?!?李建國笑著接過保溫杯,里面是他最愛喝的菊花茶。他知道,未來還有更多技術(shù)挑戰(zhàn)等著他,但有家人的支持,有團(tuán)隊的配合,他有信心把猿媛電商的技術(shù)架構(gòu)做得更穩(wěn)定、更強大 —— 就像猿媛電商的成長一樣,從 “小破屋” 到 “高樓大廈”,一步一個腳印,踏實向前。
猿媛電商的 6 位異常碼規(guī)范剛在全服務(wù)落地滿三周,雙十一大促前的壓力測試就炸出了新漏洞:一批用戶在 “優(yōu)惠券 + 滿減” 疊加下單時,前端同時彈出 “160102 - 訂單類型錯誤” 與 “150401 - 費用計算失敗” 兩個異常提示,后端日志里訂單服務(wù)和營銷服務(wù)的異常碼各執(zhí)一詞,排查兩小時才發(fā)現(xiàn) —— 營銷服務(wù)計算滿減時超時,卻未拋專屬的 “150402 - 滿減計算超時”,反而復(fù)用了訂單服務(wù)的通用錯誤碼,導(dǎo)致鏈路異常 “串線”。
更棘手的挑戰(zhàn)接踵而至:新增的生鮮業(yè)務(wù)要求異常碼關(guān)聯(lián) “冷鏈中斷” 等特殊場景,6 位編碼的模塊位已不夠分配;海外站用戶投訴 “錯誤提示全是中文”,多語言適配要如何與異常碼綁定?
李建國團(tuán)隊連夜啟動 “異常碼治理 2.0” 計劃:既要給異常碼加 “鏈路 ID” 實現(xiàn)跨服務(wù)溯源,還要設(shè)計可擴(kuò)展的編碼規(guī)則??删驮诜桨冈u審當(dāng)天,運維團(tuán)隊突然上報 —— 生產(chǎn)環(huán)境出現(xiàn) “異常碼雪崩”,近千條錯誤日志里,不同服務(wù)的異常碼竟指向同一個不存在的模塊位…… 這場突如其來的危機,會讓之前的治理成果功虧一簣嗎?歡迎評論后續(xù)精彩故事。



























