監控螢幕的綠光映在李工臉上,像某種不祥的預兆。
“錯誤型別分佈調出來了。”他敲了幾下鍵盤,螢幕上跳出密密麻麻的圖表,“你看,主要是兩種:資料庫連線超時和唯一約束衝突。”
林夕盯著“唯一約束衝突”那幾個字。會員兌換記錄表裏,每個兌換訂單應該有唯一的業務流水號。衝突意味著——有重複的流水號試圖插入。
“衝突數量?”
“從下午兩點到現在,37次。”李工放大時間軸,“分佈很均勻,大概每小時10次。”
每小時10次衝突,在每天數十萬兌換請求的總量裏,確實微不足道。但就像精密儀器裏混進的一粒沙,它不應該出現。
“能查到具體是哪些請求衝突嗎?”林夕問。
李工搖搖頭:“生產資料庫的binlog(二進製日誌)隻保留七天,而且查這種細節需要DBA(資料庫管理員)許可權,我沒有。”他頓了頓,“不過我可以看慢查詢日誌,至少知道是哪些SQL語句出問題了。”
慢查詢日誌開啟,一行行記錄滾動過去。林夕很快捕捉到了規律——出問題的都是同一個INSERT語句,向兌換記錄表插入資料。衝突的欄位是`order_no`訂單號。
“訂單號怎麽生成的?”他問。
“前端生成,UUID(通用唯一識別碼),應該不會重複啊。”李工也覺得奇怪,“除非……”
兩人對視一眼,同時想到了那個可能性:同一個訂單號,被用了兩次。
“有沒有可能,”林夕慢慢地說,“是前端生成了訂單號,發起請求,但因為網路問題沒收到響應,於是前端又用同一個訂單號重試了?”
“但防重複提交機製應該阻止這種重試。”李工說完,自己停住了,“除非那個機製失效了。”
他們一起看向監控螢幕上那個微小的錯誤率波動曲線。0.05%的錯誤率,如果換算成具體數字,大概每2000次請求會有一次失敗。而兌換介麵今天的請求量是……林夕快速心算,大約80萬次。
也就是說,大概有400次異常。
“37次唯一約束衝突,隻是冰山露出水麵的部分。”林夕低聲說,“更多請求可能根本就沒到資料庫這層,在前端或閘道器就被攔截了。”
李工沉默了幾秒:“我去申請更高許可權,查binlog。但這事兒得先跟周凱說一聲。”
“他讓我私下查,不要聲張。”林夕想起蘇晴的叮囑。
“那也得說。萬一真是係統問題,瞞不住的。”李工已經起身,“我去找他,你繼續分析。”
林夕回到工位時,已經晚上八點。開放式辦公區空了大半,隻有幾個加班的程式設計師戴著耳機,沉浸在程式碼世界裏。他的螢幕還停留在技術優化方案的檔案頁麵,遊標在“建議將防重複提交時間視窗從60秒調整為5秒”這句話後麵閃爍。
他刪掉了這句話,重新寫:“建議重新評估防重複提交機製在高並發場景下的有效性,並考慮引入分散式鎖方案。”
剛寫完,周凱的微信訊息跳出來:“來小會議室。”
小會議室裏隻有周凱一個人。他麵前的膝上型電腦開著,螢幕上正是兌換介麵的錯誤率監控。
“坐。”周凱的表情很平靜,“李工跟我說了。你覺得問題出在哪?”
林夕沒有立刻回答。他在觀察——周凱的語氣、神態、手指在觸控板上的無意識滑動。這是《孫子兵法》裏最基本的“知彼”:先瞭解對方的狀態,再決定如何應對。
“從現有資料看,可能是防重複提交機製在某些邊界情況下失效了。”林夕選擇用中性的描述,“加上今天上線的彈窗功能增加了使用者等待時間,讓一些原本不會重試的請求發生了重試。”
“你是說,我們上線了一個有問題的功能?”周凱問。
“功能本身沒問題,但它暴露了係統原有的潛在問題。”林夕糾正道,“就像一麵鏡子,照出了牆上原本就有的裂縫。”
周凱笑了笑,這個笑容裏有些林夕看不懂的東西:“很形象的比喻。那你覺得該怎麽修?”
“短期,可以先把時間視窗調回5秒,減少重試視窗。中期,需要引入更可靠的冪等機製,比如讓後端生成訂單號。長期——”林夕停頓了一下,“可能需要重構整個兌換流程的並發控製。”
“很好的思路。”周凱點頭,“但你想過沒有,為什麽三年前陳啟明設的是5秒,而我後來要改成60秒?”
林夕當然想過,但他沒說話,等待下文。
“因為使用者體驗。”周凱身體前傾,雙手交叉放在桌上,“5秒太短了。使用者點完兌換,如果頁麵卡一下,超過5秒,他再點就會報‘操作過於頻繁’。使用者看不懂技術錯誤,隻會覺得我們係統爛。所以我改成了60秒——給使用者足夠的耐心。”
“但在高並發下……”
“在高並發下,我們需要平衡。”周凱打斷他,“平衡技術完美和業務需求。小林,你技術很好,也很細心,這是優點。但你要學會從更高的角度看問題。偶爾幾十個訂單衝突,對比我們每天處理的百萬級訂單,影響微乎其微。而如果我們為了追求技術上的‘零錯誤’,讓使用者體驗變差,那纔是真正的損失。”
這番話說得滴水不漏,甚至很有道理。林夕幾乎要被說服了。
但資料在說話。那些隱藏在水麵下的異常請求,那些可能已經發生的重複扣款——對係統來說也許是百萬分之一,但對每一個遇到問題的使用者來說,就是百分之百。
“我明白了。”林夕說,“那接下來怎麽處理?”
“兩個方案。”周凱豎起兩根手指,“第一,保守方案:我們把時間視窗調回5秒,明天白天就改。這樣最安全,但可能引來一些使用者投訴。第二,激進一點:我們加強監控,觀察兩天,如果錯誤率不再上升,就維持現狀。同時你繼續寫優化方案,我們下個版本徹底解決。”
“您傾向於哪個?”林夕問。
周凱沒有直接回答:“你是這個功能的負責人,你覺得呢?”
一個溫柔的陷阱。無論林夕選哪個,後果都要自己承擔——選第一個,如果引發使用者投訴,是“不考慮使用者體驗”;選第二個,如果問題擴大,是“判斷失誤”。
林夕沉默了幾秒,然後說:“我需要更多資料來做判斷。能不能給我更高許可權,讓我查一下具體的衝突訂單,看看影響範圍到底有多大?”
周凱臉上的笑容淡了些:“生產資料許可權很敏感,需要層層審批。這樣吧,我給你開個臨時許可權,但隻能查今天的資料,而且不能匯出。可以嗎?”
“可以,謝謝凱哥。”
許可權在十分鍾後開通。林夕回到工位,開始查詢。
查詢結果讓他倒吸一口涼氣。
37次唯一約束衝突,對應的使用者有31人。其中4個使用者遇到了兩次衝突,2個使用者遇到了三次。這些衝突訂單的時間分佈很集中——都在下午兩點到四點之間,也就是彈窗功能上線後的第一個高峰期。
更關鍵的是,通過關聯查詢,他發現這31個使用者中,有19人隨後又發起了新的、成功的兌換請求。這意味著什麽?
意味著他們在第一次兌換“看似失敗”後,又重新操作,並且成功了。而第一次的請求,可能已經在後台處理成功,隻是前端沒收到響應。
林夕快速計算了一下:如果這19個使用者都被重複扣了積分,那每個人損失幾百到幾千積分不等,按兌換比例換算成現金,大概……
他的計算被手機震動打斷。是蘇晴打來的。
“林夕,你在公司嗎?”她的聲音有些急促,“客服那邊收到集中反饋了,有十幾個使用者都說積分兌換出了問題。有的說扣了積分沒收到商品,有的說被扣了兩次。客服已經轉過來五個工單了。”
“什麽時候開始的?”
“就這半小時。我們剛剛在內部群裏同步了情況。”蘇晴停頓了一下,“周凱說讓你先別動,他馬上組織會議。”
會議室裏很快坐滿了人。產品、技術、測試、客服的代表都在。氣氛凝重。
周凱主持會議,語氣依然沉穩:“情況大家都知道了。目前收到11個使用者反饋,問題集中在積分兌換。蘇晴,你把工單情況說一下。”
蘇晴開啟投影,列出了工單概要:使用者反饋的問題確實如她所說,集中在重複扣款和兌換失敗上。時間都在下午。
“技術側初步分析,”周凱繼續說,“可能是今天上線的彈窗功能,與係統原有的防重複提交機製有相容性問題。小林,你給大家同步一下你的發現。”
林夕站起來,走到投影前。他沒有直接說“時間視窗”的曆史,而是展示了那37次資料庫衝突的資料,以及關聯分析出的19個可能重複扣款的使用者。
“所以,問題根源可能是防重複提交機製在高並發下的失效。”他總結道,“彈窗延長了使用者等待時間,放大了這個問題。”
“解決方案呢?”一個產品經理問。
“短期,可以立刻把防重複提交的時間視窗從60秒調整為5秒,減少重試機會。同時後端增加對重複訂單的攔截和告警。”林夕說,“長期,需要重構整個並發控製。”
會議室裏議論紛紛。
“改回5秒,使用者投訴怎麽辦?”有人問。
“比現在使用者被重複扣款的投訴要好。”蘇晴冷靜地說,“我是產品,我必須站在使用者這邊。技術問題可以優化,但使用者信任一旦丟了,很難挽回。”
周凱抬手示意安靜:“這樣,我們分兩步走。第一,立刻回滾彈窗功能——這個改動最小,風險最低。第二,技術側評估,是否需要在今晚就調整時間視窗引數。”
“彈窗功能是市場部的緊急需求。”有人提醒。
“出了問題,再緊急的需求也得讓路。”周凱果斷地說,“小林,你負責回滾。李工,你準備調整引數,但先別執行,等我通知。”
會議在九點半結束。林夕回到工位,開始回滾程式碼。這個過程很簡單,隻需要撤銷今天的提交,重新發布。
十點,回滾完成。監控顯示,兌換介麵的錯誤率開始下降,從0.05%降到了0.03%。
但使用者反饋沒有停止。十點半,客服工單增加到了23個。
周凱的辦公室燈還亮著。林夕走過去時,門虛掩著。他聽到周凱在打電話:
“……我知道,但現在是穩定第一。彈窗已經回滾了,引數調整……再觀察一下。對,使用者反饋我們正在處理,會做好補償……”
語氣裏有種林夕從未聽過的疲憊。
他敲了敲門。
周凱結束通話電話,揉了揉眉心:“進來。回滾好了?”
“好了。錯誤率在下降,但沒完全恢複到之前水平。”林夕說,“我覺得,引數可能還是得調。”
周凱看著他,沉默了很久。窗外,城市的霓虹在夜色中流動,映在他鏡片上,讓人看不清眼神。
“小林,”他緩緩開口,“你知道三年前那場事故,最後賠了多少錢嗎?”
林夕搖頭。
“一百七十萬。”周凱說,“不是現金賠償,是等額的積分、優惠券、會員權益。陳啟明當時堅持要立刻修複資料庫主從問題,但我建議先做使用者安撫,等技術方案成熟再說。我們吵了一架。最後他妥協了,但心也涼了。”
他頓了頓:“我後來改那個時間引數,也是想避免再次走到那一步——既要技術完美,又要業務不受影響,哪有那麽容易?”
這話裏有真切的無奈,也有某種自我辯護。
“但如果現在不調,問題可能會擴大。”林夕堅持道。
周凱站起身,走到窗邊,背對著林夕:“你知道嗎,有時候做技術決策,不能隻看資料。還要看時機,看人心,看代價。”
他轉過身,臉上是林夕完全看不懂的表情:“引數可以調。但不是現在。等今晚淩晨三點,資料庫做主從重建的時候,一起改。那時候流量最低,影響最小。”
“那使用者的損失……”
“客服會處理,該補償的補償。”周凱說,“這件事我會負責。你回去休息吧,今天辛苦了。”
林夕走出辦公室時,總覺得哪裏不對。周凱的態度轉變太突然了——從堅持“使用者體驗”,到同意深夜修改關鍵引數。
回到工位,他看了一眼監控。錯誤率還在緩慢下降,但速度越來越慢。0.03%,0.029%,0.028%……
像一根被輕輕拉住的繩子,不知道什麽時候會突然繃緊。
他開啟資料庫查詢界麵,想做最後一次檢查。但臨時許可權已經過期了——周凱給的許可權隻有幾個小時。
在頁麵關閉前的最後一秒,林夕瞥見查詢曆史裏的一條記錄。不是他查的。
查詢語句是:“SELECT COUNT() FROM exchange_log WHERE create_time > u00272023-07-13 14:00:00u0027 AND error_code u003d u0027DUPLICATE_ORDER”
查詢時間:晚上九點零五分。
那時候,周凱正在主持會議。
而查詢結果,顯示在曆史記錄的最後一行:
327
不是37。
是327。
林夕盯著那個數字,後背一陣發涼。
有人在九點零五分就知道了真實的資料規模,但在會議上,所有人都以為隻有37次衝突。
誤差不是十倍。
是十倍。
夜色如墨,吞噬了窗外最後一點天光。遠處的城市燈光依然璀璨,但林夕知道,就在這片璀璨之下,有些東西已經開始失控了。
而那個真正知道失控規模的人,選擇了沉默。
為什麽?