← Lock

第 1 章:第一章:現在——裂縫

凌晨三點十七分。

客廳的燈沒開,只有筆電螢幕的光映在陳志明臉上,把他眼下的青黑照得像瘀青。他坐在地板上,背靠著沙發邊緣,兩腿伸直,左腳腳趾夾著一個已經捏扁的啤酒鋁罐。旁邊地上還有四個,排得不算整齊,像某種失敗的儀式。

他已經不記得自己是什麼時候從沙發滑到地板上的。

手機倒扣在茶几上,螢幕朝下。不是因為他不想看,而是他已經看了太多次——四十七條未讀訊息。公司群組二十三條,副總私訊兩條,人資四條,雅婷十八條。他一條一條點開,讀完,鎖屏,放下,再拿起來。循環到最後,他索性把手機翻過去,好像這樣那些字就會消失。

但不會。

他伸手把手機翻過來,解鎖。公司群組的最新訊息停留在十一分鐘前,大衛發的:「Zhi哥,你還在嗎?」

志明沒有回。

他不知道自己該回什麼。說「在」?然後呢?說「我很好」?他一點都不好。說「對不起」?這句話他在腦海裡排練了五天,每一版本都覺得不對——太輕、太重、太像在求饒、太像在卸責。

他把手機放下,目光回到筆電螢幕。

Visual Studio Code 開著一個檔案:app/Http/Controllers/PaymentController.php。游標停在第 67 行,已經停了不知道多久。那行是一個簡單的 Eloquent 查詢:

$order = Order::where('transaction_id', $request->transaction_id)->first();

就是這行。

志明盯著它,像盯著一個人的臉。他知道問題在哪裡——第 67 行。不是這行本身寫錯了,而是這行不夠。它應該長這樣:

$order = Order::where('transaction_id', $request->transaction_id)->lockForUpdate()->first();

一個 lockForUpdate()。就這三個字。

他在 GitHub 上看過。Issue #247,標題寫著「Duplicate charge under concurrent requests」,open 了八個月,沒有 maintainer 回應。下面有十三個人留言說遇到了同樣的問題,有人附了 log,有人提了 PR 但一直沒被 merge。志明那時候掃了一眼,心想:「應該不會那麼巧。」

應該不會那麼巧。

他閉上眼睛,額頭靠上沙發的布面。啤酒的味道從鋁罐口飄出來,酸酸的,混著某種腐敗的甜。


閃回是從胃開始的。

不是畫面,是一種感覺——胸腔裡有什麼東西突然縮緊,像被一隻手攥住。然後畫面才跟上來。

是上個週六。下午兩點。公司八樓的會議室。

他記得那間會議室的燈是暖黃色的,二十幾個人擠在裡面,空氣裡有咖啡和汗的氣味。他站在投影幕前面,雷射筆的紅點在投影片上發抖——後來他才意識到是自己的手在抖。

「……初步判斷是系統在處理金流回調時,在高併發情境下產生了重複扣款。」他的聲音還算穩,至少他自己覺得。「影響範圍大約兩百位客戶,金額已經在處理退款……」

他沒有繼續說下去,因為林副總舉手了。

林副總坐在長桌最靠門的位置,襯衫袖子捲到肘間,表情不是憤怒,是一種更難應付的東西——失望。或者說,是一種經過計算的失望。

「志明,」副總說,語速比平常慢,像是在對一個不太聰明的人解釋事情,「我現在需要一個明確的答案。這個技術選型,是誰決定的?」

志明記得自己當時愣了不到一秒。但那一秒裡,很多東西閃過去了——會議上副總說「這件事你看著辦」的時候,大衛舉手說「這個 Package 好像不太穩定」的時候,老張在角落裡微微搖頭的時候。

「我決定的。」志明說。

副總點了點頭,那個點頭很輕,像在說「好,那就這樣了」。然後他轉向會議室裡的其他人:「各位,這個技術選型是志明全權決定的。我當時充分信任他的專業判斷。」

信任。

志明現在坐在客廳地板上,嘴裡默念這個詞。信任。這個詞從副總嘴裡說出來的時候,聽起來不像是信任,更像是——蓋章。像在文件上蓋一個「已確認」的章,然後把文件歸檔,鎖進抽屜,跟自己再也沒有關係。

他記得自己當時看向大衛。

大衛坐在會議桌的另一端,低著頭,手指在筆電鍵盤上沒有動。他的表情不是驚訝,是一種更複雜的東西——像是一直知道這天會來,但真的來了以後,還是不知道該怎麼辦。

志明想起更早之前,週四晚上,他在公司走廊上遇到大衛。大衛從茶水間出來,手裡拿著咖啡,看到志明,腳步頓了一下。

「Zhi哥,」大衛說,聲音比平常小,「那個 webhook 的 lock,真的要加嗎?我怕時程來不及——」

「先不用,」志明那時候說,「前端加個 debounce 就好,API 層之後再補。」

大衛張了張嘴,像是想說什麼,但最後只是點了一下頭。

那個欲言又止的臉,現在想起來,比任何指責都重。


志明睜開眼睛。

游標還在第 67 行。他把手放到鍵盤上,手指懸在 lockForUpdate() 的位置,沒有按下去。

改又有什麼用?系統已經出事了。兩百個客戶被重複扣款。媒體報導出來了——「電商平台重複扣款,消費者投訴無門」。公司官網的留言區被灌爆,有人說要提告,有人說要投訴消基會。客服電話被打爆,據說有一個客服人員講到哭。

而他,陳志明,三十四歲,資深後端工程師,技術小組長,月薪六萬五,老婆懷著第二胎七個月,大女兒四歲——坐在客廳地板上,捏著啤酒罐,看著一段他已經看了五百遍的程式碼。

他忽然覺得很荒謬。

不是那種「人生好荒謬」的荒謬,是一種很具體的荒謬:他花了八年時間學會怎麼寫乾淨的 code、怎麼設計一個好的架構、怎麼在 Laravel 裡正確地使用 Service Container 和 Middleware,結果壓垮他的不是一個多難的技術問題,而是一個他明明知道、卻選擇忽略的 lock。

不是不會。是覺得不會那麼巧。


背後傳來腳步聲。

很輕,是室內拖鞋踩在木地板上的聲音。志明沒有回頭,但他知道是誰。

雅婷站在他身後。

沉默大概持續了十秒。然後他感覺到一件外套披在他肩上——是他們結婚時買的那件灰色羽絨外套,有他的體溫,也有洗衣精的味道。

雅婷沒有說話。她站在那裡,一隻手還搭在他肩膀上,手指微微收緊,像是在確認他是真實的。

志明低著頭,看著自己放在鍵盤上的手。

「你已經坐在這裡五天了。」雅婷的聲音很平,不是指責,也不是心疼,是一種介于兩者之間的、很疲憊的陳述。

「我知道。」

「我只是……需要想清楚。」

雅婷的手從他肩膀上移開。她沒有走,但也沒有坐下來。就站在那裡,在他和筆電螢幕的光之間,形成一個剪影。胸前的睡衣鈕扣扣錯了一格——志明注意到了,但他沒有說。結婚六年了,他知道那是她在他去見副總之前匆忙套上的睡袍,來不及換。

「想清楚什麼?」

志明張了張嘴。

他想說很多話。想說他搞不懂副總怎麼可以把「信任」說得那麼乾淨俐落。想說他不確定大衛在測試報告裡寫的那行小字算不算「說了」。想說他不確定老張在會議上那個搖頭算不算「反對」。想說他不確定自己當初說「先用了,有問題再說」的時候,是出於判斷還是出於恐懼——恐懼副總覺得他扛不住、恐懼團隊覺得他不夠果決、恐懼自己看起來像個會「要求延期」的主管。

他想說:我不知道我是不是霸凌者。

但他什麼都沒說。

沉默在客廳裡蔓延開來,像水滲進沙地裡。

雅婷最終沒有再問。她轉身走回臥室,腳步聲很輕,門關上的聲音也很輕。但志明知道她沒有睡。他們結婚六年了,他聽得出她什麼時候是真的在睡、什麼時候是在等他開口。

他拉了拉肩上的外套,低下頭,重新看著螢幕。

游標還在第 67 行。

他開始打字。不是改 code,而是打開一個新的檔案,空白的,什麼都沒有。他打了幾個字,刪掉,再打,再刪。最後他停下來,看著空白的檔案,心裡有一個念頭浮上來——

如果那天我做了不同決定。

如果他在會議上說「這個 Package 有風險,我需要多兩週做壓力測試」。如果他在茶水間聽完大衛的問題以後說「好,我們來加 lock」。如果他看到老張搖頭的時候停下來問「你覺得哪裡有問題」。如果他那天晚上沒有回「快了」,而是真的回家。

如果。

如果。

如果。

他閉上眼睛。客廳很安靜,只有筆電風扇的聲音,嗡嗡的,像某種遠處的、不屬於這個夜晚的噪音。

然後,畫面開始模糊。

不是他睡著了。是記憶在倒帶。

他感覺到自己正在從這個客廳、這個地板、這些啤酒罐、這件灰色羽絨外套裡,一點一點地往後退。凌晨三點十七分變成凌晨三點十六分,再變成凌晨三點十五分——不,不是鬧鐘在倒轉,是時間本身。

他看見自己打開筆電。看見自己拿起手機。看見自己坐在客廳地板上。看見自己從沙發上滑下來。看見自己打開第一罐啤酒。看見自己關掉辦公室的燈。看見自己走出公司大門。看見自己——

一直退。

一直退。

退到五天前的某個起點。

退到一切還沒壞掉的時候。

退到那個星期一的會議室。

退到副總說「我相信你的判斷」的那一刻。

退到志明說「好」的前一秒——

那個「好」字還懸在空氣裡,還沒落地。

一切還來得及。


(第一章 完)

3681 字 •