国产av一二三区|日本不卡动作网站|黄色天天久久影片|99草成人免费在线视频|AV三级片成人电影在线|成年人aV不卡免费播放|日韩无码成人一级片视频|人人看人人玩开心色AV|人妻系列在线观看|亚洲av无码一区二区三区在线播放

網(wǎng)易首頁 > 網(wǎng)易號 > 正文 申請入駐

一行UPDATE語句讓500萬蒸發(fā),這個(gè)程序員用7張表重建了銀行

0
分享至


2023年某支付平臺故障,用戶余額憑空多出一個(gè)零。工程師排查了72小時(shí),最終發(fā)現(xiàn)是一行UPDATE語句在并發(fā)時(shí)撞上了 race condition(競態(tài)條件)。錢從哪來?到哪去?沒人說得清。

這種事故在金融科技領(lǐng)域不算新聞。單字段存余額的設(shè)計(jì),就像把現(xiàn)金鎖在抽屜里卻不記賬——抽屜一亂,全盤皆輸。

Paul Babatuyi 在 GitHub 開源的 double-entry-bank 項(xiàng)目,用 Go 和 PostgreSQL 演示了另一種思路:雙分錄記賬。每筆交易生成兩條記錄,一借一貸,相互勾稽。沒有 UPDATE,只有 INSERT;沒有刪除,只有追加。賬本變成只讀的歷史流,任何異常都能被追溯。

為什么單字段余額是定時(shí)炸彈

開發(fā)者常犯的錯(cuò),是把"余額"當(dāng)成一個(gè)可以隨便改的數(shù)字。UPDATE accounts SET balance = balance - 100 看起來無害,實(shí)則埋了三顆雷。

第一顆是原子性。 兩條并發(fā)請求同時(shí)讀取余額1000,各自減去100,最終寫入900。實(shí)際應(yīng)該扣200,系統(tǒng)卻只扣了100。這種"丟失更新"在流量高峰時(shí)幾乎必然發(fā)生。

第二顆是可審計(jì)性。 余額字段被覆蓋后,舊值永久消失。用戶投訴"錢少了",你只能看到當(dāng)前數(shù)字,無法還原變化路徑。監(jiān)管合規(guī)?更是無從談起。

第三顆是人為風(fēng)險(xiǎn)。 擁有 UPDATE 權(quán)限的工程師,理論上可以靜默修改任意賬戶。沒有日志交叉驗(yàn)證,內(nèi)部舞弊難以被發(fā)現(xiàn)。

雙分錄記賬的解法很古老——1494年盧卡·帕喬利在《算術(shù)、幾何、比及比例概要》中系統(tǒng)闡述的復(fù)式記賬,至今仍是現(xiàn)代金融的基石。每一筆資金流動必須同時(shí)記錄來源和去向,總額恒等。

7張表如何鎖住每一分錢

Paul 的數(shù)據(jù)庫設(shè)計(jì)沒有魔法,只是把"余額"這個(gè)概念拆解成了不可變的事件流。

核心表只有三張:accounts(賬戶定義)、entries(分錄明細(xì))、transfers(交易主檔)。accounts 記錄賬戶類型和幣種,余額不存這里。entries 是真正的賬本,每條記錄包含賬戶ID、金額、借貸方向,以及指向 transfers 的外鍵。transfers 則保存交易元數(shù)據(jù):發(fā)起方、接收方、時(shí)間戳、外部流水號。


查詢余額時(shí),不再是 SELECT balance FROM accounts,而是 SUM(entries.amount) WHERE account_id = X。實(shí)時(shí)計(jì)算聽起來費(fèi)性能?PostgreSQL 的索引和物化視圖能搞定,或者像 Stripe 那樣異步匯總到緩存——但緩存只是只讀副本,真相永遠(yuǎn)在 entries 表里。

另外四張表處理支撐功能:users、sessions、verify_emails 管身份,還有一張參數(shù)表存配置。業(yè)務(wù)表與支撐表隔離,權(quán)限粒度可以收得很細(xì)。

sqlc 把 SQL 變成類型安全的 Go 代碼

手寫數(shù)據(jù)庫操作容易出錯(cuò)。字段名拼錯(cuò)、類型對不上、NULL 處理遺漏——這些 bug 在編譯期發(fā)現(xiàn)不了,上線后才炸。

sqlc 的解決思路是反向生成:你先寫 SQL 查詢,它自動生成對應(yīng)的 Go 結(jié)構(gòu)和接口。查詢文件放在 postgres/queries/ 目錄,sqlc.yaml 配置好連接信息,執(zhí)行 sqlc generate 就能得到 db.go、models.go、querier.go 三個(gè)文件。

好處是 SQL 和 Go 的類型系統(tǒng)打通。你在查詢里寫 SELECT id, owner, balance FROM accounts,生成的代碼就返回包含這三個(gè)字段的結(jié)構(gòu)體。改表結(jié)構(gòu)?改 SQL 文件重新生成,編譯錯(cuò)誤會精準(zhǔn)定位到所有受影響的地方。

Paul 的遷移文件用 golang-migrate 管理,版本號順序執(zhí)行,回滾也有對應(yīng)腳本。這套組合讓數(shù)據(jù)庫變更可 review、可回滾、可追蹤——和雙分錄記賬的"不可變"哲學(xué)一脈相承。

Store 層:事務(wù)重試與死鎖防御

雙分錄記賬的寫入比單字段復(fù)雜得多。一筆轉(zhuǎn)賬要同時(shí)插入 transfers 記錄和兩條 entries 記錄,三者必須在同一個(gè)數(shù)據(jù)庫事務(wù)里。

但事務(wù)帶來新問題:并發(fā)寫入相同賬戶時(shí),PostgreSQL 的行鎖可能引發(fā)死鎖。Paul 的解法是在 Store 層封裝 ExecTx 函數(shù),自動檢測 pgx.ErrTxCommitRollback 錯(cuò)誤,用指數(shù)退避算法重試。重試邏輯寫在代碼里,而不是拋給調(diào)用方處理。

更細(xì)的設(shè)計(jì)是隔離級別。默認(rèn)的 Read Committed 在多數(shù)場景夠用,但涉及余額校驗(yàn)時(shí)可能需要 Repeatable Read。Paul 把隔離級別作為參數(shù)暴露,讓業(yè)務(wù)層按需選擇。

測試覆蓋了極端場景:并發(fā)轉(zhuǎn)賬、部分失敗、網(wǎng)絡(luò)中斷后的狀態(tài)一致性。用的不是 mock,而是 testcontainers 啟動真實(shí) PostgreSQL 容器,確保測試通過即生產(chǎn)可用。


Service 層:業(yè)務(wù)規(guī)則與賬本操作解耦

Store 層只管"怎么存",Service 層決定"存什么"。這種分層讓單元測試可以 mock 數(shù)據(jù)庫,也讓業(yè)務(wù)邏輯不被 SQL 細(xì)節(jié)污染。

轉(zhuǎn)賬流程在 Service 層被拆解為:校驗(yàn)身份→檢查余額充足→創(chuàng)建 transfer 記錄→生成借貸 entries→提交事務(wù)。任何一步失敗,整個(gè)事務(wù)回滾,不會出現(xiàn)"錢扣了但沒到賬"的中間狀態(tài)。

余額檢查的實(shí)現(xiàn)值得一提。因?yàn)?entries 表只增不改,"當(dāng)前余額"是聚合計(jì)算的結(jié)果。Paul 在代碼里用 SELECT FOR UPDATE 鎖定相關(guān)賬戶的 entries 插入權(quán)限,同時(shí)計(jì)算 SUM(amount) 得到實(shí)時(shí)余額。這比 SELECT FOR UPDATE 鎖整張表更精細(xì),并發(fā)性能更好。

貨幣處理也埋了細(xì)節(jié)。金額用 bigint 存最小單位(分),避免浮點(diǎn)誤差。幣種代碼硬校驗(yàn),防止把 USD 和 EUR 混算。這些約束在數(shù)據(jù)庫層和代碼層各設(shè)一道,雙保險(xiǎn)。

從演示項(xiàng)目到生產(chǎn)系統(tǒng)

golangbank.app 的在線演示很完整:注冊、登錄、轉(zhuǎn)賬、查流水、導(dǎo)出報(bào)表。Swagger 文檔自動生成,前端用 Next.js 實(shí)現(xiàn),前后端分離部署。

但生產(chǎn)環(huán)境還要補(bǔ)很多課。審計(jì)日志需要獨(dú)立存儲,防篡改;敏感操作要雙因素認(rèn)證;大額轉(zhuǎn)賬延遲結(jié)算,預(yù)留風(fēng)控窗口。Paul 的項(xiàng)目是骨架,血肉需要按業(yè)務(wù)填充。

一個(gè)值得參考的演進(jìn)方向是事件溯源(Event Sourcing)。entries 表本質(zhì)上已經(jīng)是事件流,如果加上 Kafka 或 Pulsar 做異步分發(fā),就能支撐更復(fù)雜的業(yè)務(wù):實(shí)時(shí)風(fēng)控、多幣種清算、甚至跨行對賬。

另一個(gè)方向是分片。entries 表隨時(shí)間膨脹,單機(jī) PostgreSQL 遲早觸頂。按賬戶ID哈希分片,或者按時(shí)間冷熱分離,都是常見策略。雙分錄記賬的優(yōu)勢在于:分片不影響一致性模型,因?yàn)槊抗P交易的借貸雙方可以落在不同分片,只要保證 transfers 記錄的完整性。

回到開頭的那行 UPDATE。在 Paul 的系統(tǒng)里,這句話永遠(yuǎn)不會出現(xiàn)。余額不是被修改的,而是被計(jì)算出來的;歷史不是被覆蓋的,而是被追加的。這種設(shè)計(jì)多寫了代碼,多占了存儲,但換來的是可審計(jì)、可回滾、可信任。

當(dāng)你的系統(tǒng)開始處理真金白銀,"簡單"往往是最貴的選擇。你愿意為這行消失的 UPDATE,多寫多少行代碼?

特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺“網(wǎng)易號”用戶上傳并發(fā)布,本平臺僅提供信息存儲服務(wù)。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相關(guān)推薦
熱點(diǎn)推薦
剿匪天王立奇功本是上將才,授銜僅大校,毛主席震怒稱不妥

剿匪天王立奇功本是上將才,授銜僅大校,毛主席震怒稱不妥

磊子講史
2026-03-19 16:17:42
我在小城市,一個(gè)人做電商,半年掙300萬

我在小城市,一個(gè)人做電商,半年掙300萬

南風(fēng)窗
2026-03-26 10:07:51
這4種魚,可能含有甲醛和重金屬,建議:還是少吃比較好!

這4種魚,可能含有甲醛和重金屬,建議:還是少吃比較好!

阿龍美食記
2026-03-24 21:52:23
炸屏跑圈!51歲林志玲跑馬拉松,生圖狀態(tài)封神,網(wǎng)友:滿50減30!

炸屏跑圈!51歲林志玲跑馬拉松,生圖狀態(tài)封神,網(wǎng)友:滿50減30!

馬拉松跑步健身
2026-03-26 22:09:16
1986年韓先楚拒絕葬在八寶山,他對陳云說:那里有我不愿見到的人

1986年韓先楚拒絕葬在八寶山,他對陳云說:那里有我不愿見到的人

百年歷史老號
2026-03-25 18:27:41
張雪峰:把陜西37所公辦本科分五檔!你家孩子讀哪一檔(珍藏版)

張雪峰:把陜西37所公辦本科分五檔!你家孩子讀哪一檔(珍藏版)

熱心市民小黃
2026-03-27 07:10:00
破防!中國油輪硬闖霍爾木茲海峽,伊朗全程護(hù)航,看完太提氣

破防!中國油輪硬闖霍爾木茲海峽,伊朗全程護(hù)航,看完太提氣

戧詞奪理
2026-03-25 10:53:15
廣東今日早報(bào)!球迷熱議楊鳴替杜鋒,徐杰深夜發(fā)聲,崔永熙回首發(fā)

廣東今日早報(bào)!球迷熱議楊鳴替杜鋒,徐杰深夜發(fā)聲,崔永熙回首發(fā)

多特體育說
2026-03-27 09:27:26
7旬男子陪老伴住院,醫(yī)生看了他一眼發(fā)現(xiàn)其面部有猝死先兆,將其從死亡邊緣拉回

7旬男子陪老伴住院,醫(yī)生看了他一眼發(fā)現(xiàn)其面部有猝死先兆,將其從死亡邊緣拉回

觀威海
2026-03-26 09:55:04
日本代表缺席,中方終于明確回應(yīng),高市算得很準(zhǔn):中國不會原諒她

日本代表缺席,中方終于明確回應(yīng),高市算得很準(zhǔn):中國不會原諒她

云鵬敘事
2026-03-27 10:14:00
第二名是最大的輸家!威少被約基奇搞得不敢退役!真相是這樣么?

第二名是最大的輸家!威少被約基奇搞得不敢退役!真相是這樣么?

五姑娘臺球
2026-03-26 09:45:52
網(wǎng)曝張雪峰立有遺囑!巨額遺產(chǎn)和股份前妻女兒妻子三人這么分:網(wǎng)友吵瘋了

網(wǎng)曝張雪峰立有遺囑!巨額遺產(chǎn)和股份前妻女兒妻子三人這么分:網(wǎng)友吵瘋了

不二表姐
2026-03-26 23:45:52
我去!內(nèi)娛最大的性丑聞,拍出來了

我去!內(nèi)娛最大的性丑聞,拍出來了

皮蛋兒電影
2026-03-04 14:39:25
美伊打23天,美方看清楚一件事,貝森特通告全球:已無法阻止中國

美伊打23天,美方看清楚一件事,貝森特通告全球:已無法阻止中國

跳跳歷史
2026-03-27 11:15:31
美國的大炮一響,伊朗賣給中國的石油,為什么反而比以前更多了?

美國的大炮一響,伊朗賣給中國的石油,為什么反而比以前更多了?

錯(cuò)過美好
2026-03-27 04:00:54
深圳雙雄互撕!大疆一紙?jiān)V狀,影石一天沒了50億

深圳雙雄互撕!大疆一紙?jiān)V狀,影石一天沒了50億

野馬財(cái)經(jīng)
2026-03-26 16:39:35
安徽一女護(hù)士回娘家路上失蹤,15年后給哥哥托夢:我在院子里

安徽一女護(hù)士回娘家路上失蹤,15年后給哥哥托夢:我在院子里

清茶淺談
2025-02-27 14:55:55
79元太火!小米磁吸玩偶賣斷貨 官方承諾加快生產(chǎn)

79元太火!小米磁吸玩偶賣斷貨 官方承諾加快生產(chǎn)

快科技
2026-03-26 07:09:03
曝張雪峰倒下30分鐘后才被發(fā)現(xiàn),飲食習(xí)慣糟糕,一口氣吃8根雪糕

曝張雪峰倒下30分鐘后才被發(fā)現(xiàn),飲食習(xí)慣糟糕,一口氣吃8根雪糕

古希臘掌管松餅的神
2026-03-25 11:08:46
2-0!意大利附加賽過首關(guān) 距世界杯僅差1場 8000萬巨星凌空斬救主

2-0!意大利附加賽過首關(guān) 距世界杯僅差1場 8000萬巨星凌空斬救主

我愛英超
2026-03-27 05:48:05
2026-03-27 12:12:49
像素與芯片
像素與芯片
有態(tài)度網(wǎng)友ytd
563文章數(shù) 2關(guān)注度
往期回顧 全部

科技要聞

OpenAI果斷砍掉"成人模式",死磕生產(chǎn)力

頭條要聞

男孩被搶走17年后找到生母 對"命好"的弟弟感情微妙

頭條要聞

男孩被搶走17年后找到生母 對"命好"的弟弟感情微妙

體育要聞

近29戰(zhàn)23勝!這支黃蜂有多強(qiáng)?

娛樂要聞

張雪峰靈堂內(nèi)景曝光,四周擺滿了鮮花

財(cái)經(jīng)要聞

很反常!油價(jià)向上,黃金向下

汽車要聞

與眾08,金標(biāo)大眾不能輸?shù)囊粦?zhàn)

態(tài)度原創(chuàng)

教育
旅游
游戲
本地
公開課

教育要聞

教育孩子,你掌握邊界感了嗎?

旅游要聞

“周末不忙,來趟宜良” ,春光爛漫,一起出門踏青去~

緊急救火!《博德3》推送百兆熱補(bǔ)丁 修復(fù)炸檔與崩潰

本地新聞

救命,這只醬板鴨已經(jīng)在我手機(jī)復(fù)仇了一萬遍

公開課

李玫瑾:為什么性格比能力更重要?

無障礙瀏覽 進(jìn)入關(guān)懷版