庫(kù)架構(gòu)風(fēng)格:數(shù)據(jù)一致性與組件解耦的架構(gòu)實(shí)踐)
我見(jiàn)過(guò)太多團(tuán)隊(duì)在架構(gòu)選型時(shí)只盯著微服務(wù)和消息隊(duì)列一提倉(cāng)庫(kù)架構(gòu)風(fēng)格就覺(jué)得是老古董或者太簡(jiǎn)單。但說(shuō)實(shí)話倉(cāng)庫(kù)架構(gòu)風(fēng)格是軟件系統(tǒng)里最常被低估、卻又最能扛住業(yè)務(wù)演進(jìn)的一種組織方式。它把組件之間的交互從互相調(diào)用改成共同訪問(wèn)一個(gè)中央數(shù)據(jù)倉(cāng)庫(kù)數(shù)據(jù)統(tǒng)一、邊界清晰、擴(kuò)展路徑明確很多數(shù)據(jù)密集型系統(tǒng)和業(yè)務(wù)管理平臺(tái)穩(wěn)定運(yùn)行五六年甚至更久底層就是這套架構(gòu)。這篇文章我會(huì)從理論到實(shí)踐把倉(cāng)庫(kù)架構(gòu)風(fēng)格拆開(kāi)講透包括它的本質(zhì)、優(yōu)缺點(diǎn)、適用場(chǎng)景以及我在實(shí)際項(xiàng)目里踩過(guò)的坑和總結(jié)出的落地要點(diǎn)。適合正在做架構(gòu)選型的開(kāi)發(fā)者、想理解經(jīng)典架構(gòu)的新人以及想給現(xiàn)有系統(tǒng)梳理優(yōu)化思路的團(tuán)隊(duì)參考。1. 先把概念說(shuō)清楚倉(cāng)庫(kù)架構(gòu)風(fēng)格到底是什么1.1 倉(cāng)庫(kù)架構(gòu)的核心定義倉(cāng)庫(kù)架構(gòu)風(fēng)格英文通常叫 Repository Architecture Style屬于典型的以數(shù)據(jù)為中心Data-Centered的架構(gòu)風(fēng)格。它的核心思想是系統(tǒng)里所有功能組件不直接互相調(diào)用而是通過(guò)一個(gè)中央數(shù)據(jù)存儲(chǔ)來(lái)協(xié)作。這個(gè)中央存儲(chǔ)可以是關(guān)系數(shù)據(jù)庫(kù)、內(nèi)存緩存、文件系統(tǒng)甚至是一個(gè)內(nèi)存中的共享數(shù)據(jù)結(jié)構(gòu)統(tǒng)稱為倉(cāng)庫(kù)Repository。組件把數(shù)據(jù)寫入倉(cāng)庫(kù)也從倉(cāng)庫(kù)讀取數(shù)據(jù)組件與組件之間的通信通過(guò)數(shù)據(jù)間接完成。用生活里的例子類比就是辦公室的共享檔案柜。一個(gè)部門需要另一個(gè)部門的資料不會(huì)跑到對(duì)方工位上隔空喊話而是把文件放進(jìn)檔案柜需要的人自己來(lái)取。這樣做的好處是各個(gè)部門不需要了解彼此的工作流程只要約定好文件格式和歸檔規(guī)則。本質(zhì)上是把系統(tǒng)里的數(shù)據(jù)流主線抽離出來(lái)作為獨(dú)立的中心節(jié)點(diǎn)存在。這種風(fēng)格的關(guān)鍵特征包括三點(diǎn)數(shù)據(jù)統(tǒng)一存儲(chǔ)、組件依賴數(shù)據(jù)契約、擴(kuò)展通過(guò)向倉(cāng)庫(kù)追加能力實(shí)現(xiàn)。理解了這三點(diǎn)后面再看它的優(yōu)勢(shì)和坑就都順了。1.2 倉(cāng)庫(kù)架構(gòu)與管道-過(guò)濾器風(fēng)格的關(guān)鍵差別很多初學(xué)者容易把倉(cāng)庫(kù)架構(gòu)和管道過(guò)濾器Pipeline-Filter架構(gòu)搞混因?yàn)閮煞N風(fēng)格都在說(shuō)組件怎么協(xié)作。實(shí)際上它們解決的是兩類完全不同的問(wèn)題。管道過(guò)濾器的核心是數(shù)據(jù)流數(shù)據(jù)從源頭進(jìn)來(lái)經(jīng)過(guò)一層一層的過(guò)濾器處理每個(gè)過(guò)濾器有輸入輸出數(shù)據(jù)是流動(dòng)的、有方向的、在一次性的管道中被加工。典型場(chǎng)景是日志處理、ETL、音視頻編碼流程。它適合處理步驟清晰、線性推進(jìn)、每個(gè)環(huán)節(jié)職責(zé)單一的業(yè)務(wù)。倉(cāng)庫(kù)架構(gòu)的核心是共享狀態(tài)數(shù)據(jù)不是流經(jīng)組件而是靜態(tài)存放在中央倉(cāng)庫(kù)里組件隨時(shí)可以取用和更新。典型場(chǎng)景是訂單系統(tǒng)、內(nèi)容管理系統(tǒng)、企業(yè)資源管理系統(tǒng)這些系統(tǒng)里多個(gè)模塊都圍繞同一套核心數(shù)據(jù)工作數(shù)據(jù)有復(fù)雜的關(guān)聯(lián)和狀態(tài)變遷。對(duì)比表格如下對(duì)比維度倉(cāng)庫(kù)架構(gòu)風(fēng)格管道-過(guò)濾器風(fēng)格組件間交互方式組件讀寫共享倉(cāng)庫(kù)間接通信組件之間通過(guò)數(shù)據(jù)流直接傳遞數(shù)據(jù)管理方式數(shù)據(jù)集中持久化、可重復(fù)訪問(wèn)數(shù)據(jù)流式傳遞、通常不持久化擴(kuò)展方式新增組件接上倉(cāng)庫(kù)即可新增過(guò)濾器串入管道適應(yīng)場(chǎng)景數(shù)據(jù)密集型、狀態(tài)復(fù)雜、需要事務(wù)和一致性流式處理、步驟清晰、線性遞增風(fēng)險(xiǎn)焦點(diǎn)倉(cāng)庫(kù)節(jié)點(diǎn)性能、并發(fā)、單點(diǎn)管道吞吐、過(guò)濾器排隊(duì)一個(gè)很現(xiàn)實(shí)的例子如果做一套在線報(bào)表統(tǒng)計(jì)系統(tǒng)數(shù)據(jù)源固定、處理鏈路固定、輸出結(jié)果固定管道過(guò)濾器非常適合。但如果你做的是電商后臺(tái)訂單、庫(kù)存、會(huì)員、營(yíng)銷幾個(gè)模塊都要頻繁讀寫同一批數(shù)據(jù)而且彼此的數(shù)據(jù)還互相影響那管道過(guò)濾器用起來(lái)會(huì)非常別扭倉(cāng)庫(kù)架構(gòu)才是順手的那套方案。1.3 倉(cāng)庫(kù)架構(gòu)常見(jiàn)的幾種變體倉(cāng)庫(kù)架構(gòu)并不是只有一個(gè)數(shù)據(jù)庫(kù)掛幾個(gè)服務(wù)這一種形態(tài)實(shí)操中常見(jiàn)的變體有三類。第一種是標(biāo)準(zhǔn)中央數(shù)據(jù)庫(kù)風(fēng)格。這是最常見(jiàn)的形態(tài)一個(gè)中心數(shù)據(jù)庫(kù)加上多個(gè)應(yīng)用組件各組件通過(guò)數(shù)據(jù)庫(kù)連接訪問(wèn)共享數(shù)據(jù)。企業(yè)管理系統(tǒng)、交易系統(tǒng)大多是這個(gè)套路。第二種是黑板系統(tǒng)。這是倉(cāng)庫(kù)架構(gòu)中更智能的一種變體中央倉(cāng)庫(kù)被稱為黑板多個(gè)獨(dú)立的知識(shí)源Knowledge Source并發(fā)地盯著黑板誰(shuí)對(duì)當(dāng)前問(wèn)題有把握誰(shuí)就上去寫一塊黑板上的內(nèi)容會(huì)逐步完善直到解決整個(gè)問(wèn)題。這個(gè)模式在語(yǔ)音識(shí)別、信號(hào)處理、專家系統(tǒng)里很常見(jiàn)。第三種是內(nèi)存式倉(cāng)庫(kù)。倉(cāng)庫(kù)不落盤放在分布式緩存或進(jìn)程內(nèi)存中追求的是極致的訪問(wèn)速度。比如秒殺系統(tǒng)的庫(kù)存模塊、游戲服務(wù)器的在線狀態(tài)管理。它犧牲了一部分持久化能力換來(lái)的是吞吐量。理解變體的意義在于選型時(shí)不要被倉(cāng)庫(kù)這個(gè)詞框住核心判斷標(biāo)準(zhǔn)是是否存在一個(gè)組件之間共享的數(shù)據(jù)中樞。只要有你就已經(jīng)在用倉(cāng)庫(kù)架構(gòu)思想了。2. 搭架構(gòu)前必須想清楚倉(cāng)庫(kù)架構(gòu)的優(yōu)勢(shì)和代價(jià)2.1 優(yōu)勢(shì)數(shù)據(jù)一致、組件解耦、擴(kuò)展直觀倉(cāng)庫(kù)架構(gòu)最吸引人的一點(diǎn)是天然的數(shù)據(jù)一致性。所有組件讀寫同一個(gè)倉(cāng)庫(kù)不存在多庫(kù)多副本之間sync的問(wèn)題數(shù)據(jù)庫(kù)的事務(wù)機(jī)制可以保證一組操作要么全部成功、要么全部回滾。對(duì)于訂單、支付、庫(kù)存這類對(duì)數(shù)據(jù)準(zhǔn)確性要求極高的業(yè)務(wù)這一點(diǎn)幾乎是剛需。組件解耦也是實(shí)打?qū)嵉膬?yōu)勢(shì)。組件A和組件B不需要知道彼此的地址、接口、實(shí)現(xiàn)細(xì)節(jié)只依賴倉(cāng)庫(kù)的數(shù)據(jù)結(jié)構(gòu)。只要數(shù)據(jù)結(jié)構(gòu)不變A怎么改都不影響B(tài)。這種解耦比微服務(wù)之間的HTTP調(diào)用解耦更徹底——畢竟連調(diào)用鏈都沒(méi)有了。擴(kuò)展起來(lái)也直觀。系統(tǒng)要增加一個(gè)新功能模塊只需要按倉(cāng)庫(kù)的數(shù)據(jù)契約對(duì)新模塊做開(kāi)發(fā)接好讀寫通道然后部署上線。不需要跟存量組件協(xié)調(diào)接口、不需要聯(lián)調(diào)數(shù)據(jù)流倉(cāng)庫(kù)架構(gòu)讓新增功能變得很像往檔案柜里加一個(gè)新的文件夾。另外由于數(shù)據(jù)集中在倉(cāng)庫(kù)備份、恢復(fù)、遷移、權(quán)限管控都變得簡(jiǎn)單。你只需要處理好一個(gè)節(jié)點(diǎn)不用在幾十個(gè)微服務(wù)之間追著數(shù)據(jù)跑。這在長(zhǎng)期運(yùn)維里節(jié)省的精力和成本非??捎^。2.2 代價(jià)瓶頸、單點(diǎn)、并發(fā)復(fù)雜度沒(méi)有哪種架構(gòu)只賺不虧倉(cāng)庫(kù)架構(gòu)的高光背后有很清楚的代價(jià)清單。第一是性能瓶頸。所有組件的數(shù)據(jù)請(qǐng)求都匯聚到倉(cāng)庫(kù)節(jié)點(diǎn)倉(cāng)庫(kù)的處理能力等于整個(gè)系統(tǒng)的處理天花板。一旦訪問(wèn)量上來(lái)數(shù)據(jù)庫(kù)CPU、磁盤IO、連接數(shù)一個(gè)接一個(gè)報(bào)警。這種情況下單純加應(yīng)用節(jié)點(diǎn)沒(méi)有用瓶頸不在應(yīng)用層。第二是單點(diǎn)風(fēng)險(xiǎn)。倉(cāng)庫(kù)如果掛了整個(gè)系統(tǒng)癱瘓。數(shù)據(jù)庫(kù)宕機(jī)、倉(cāng)庫(kù)服務(wù)故障、網(wǎng)絡(luò)分區(qū)都可能讓所有組件一起不可用。于是你必須花額外精力做高可用主從復(fù)制、故障切換、多活集群。第三是并發(fā)控制復(fù)雜。多個(gè)組件同時(shí)寫同一個(gè)共享數(shù)據(jù)鎖、事務(wù)、隔離級(jí)別、死鎖處理全部要仔細(xì)設(shè)計(jì)。數(shù)據(jù)越集中并發(fā)控制問(wèn)題的出現(xiàn)概率越高排查難度也越大。第四是數(shù)據(jù)模型變更牽一發(fā)動(dòng)全身。倉(cāng)庫(kù)表結(jié)構(gòu)一變所有讀寫它的組件都得跟著改而且必須安排灰度順序。字段改名、類型變更在微服務(wù)架構(gòu)里可能只影響一個(gè)服務(wù)在倉(cāng)庫(kù)架構(gòu)里可能影響一整個(gè)系統(tǒng)。把這些代價(jià)擺出來(lái)不是勸退而是希望大家在選型時(shí)做一個(gè)清醒的權(quán)衡一致性、解耦、集中管理是否比性能彈性、服務(wù)隔離、模型獨(dú)立更貼合你當(dāng)前業(yè)務(wù)的核心訴求。3. 從理論到實(shí)踐一個(gè)典型業(yè)務(wù)的倉(cāng)庫(kù)架構(gòu)落地過(guò)程3.1 場(chǎng)景設(shè)定與選型推理為了把抽象的理論落到可操作層面我用一個(gè)實(shí)際做過(guò)的模擬項(xiàng)目來(lái)說(shuō)明我們叫它某跨平臺(tái)訂單管理系統(tǒng)。需求大致是系統(tǒng)里有商品中心、庫(kù)存中心、訂單中心、會(huì)員中心四個(gè)業(yè)務(wù)域它們需要圍繞訂單生命周期緊密協(xié)同。用戶下單時(shí)系統(tǒng)要校驗(yàn)庫(kù)存、扣減庫(kù)存、創(chuàng)建訂單、更新會(huì)員等級(jí)和積分四件事必須有一致性保證不能出現(xiàn)訂單創(chuàng)建了但庫(kù)存沒(méi)扣、或者積分加了但訂單失敗的情況。選型時(shí)擺在我們面前的有三條路微服務(wù)、事件驅(qū)動(dòng)、倉(cāng)庫(kù)架構(gòu)。微服務(wù)雖然每個(gè)域獨(dú)立部署很誘人但跨服務(wù)事務(wù)一致性落地成本太高Saga分布式事務(wù)的補(bǔ)償邏輯非常復(fù)雜開(kāi)發(fā)周期會(huì)拉長(zhǎng)。事件驅(qū)動(dòng)在訂單這種強(qiáng)一致場(chǎng)景下同樣不好處理消息可能丟失、可能重復(fù)消費(fèi)要做到嚴(yán)格一致需要額外建設(shè)。倉(cāng)庫(kù)架構(gòu)剛好匹配這個(gè)業(yè)務(wù)的核心特征共享數(shù)據(jù)、強(qiáng)一致、事務(wù)邊界清晰。所以我們最終選了倉(cāng)庫(kù)架構(gòu)。這次選型給我一個(gè)很深的體會(huì)架構(gòu)風(fēng)格沒(méi)有絕對(duì)好壞只有匹配不匹配。當(dāng)前業(yè)務(wù)是高一致性要求、中等并發(fā)量、數(shù)據(jù)模型密集關(guān)聯(lián)的倉(cāng)庫(kù)架構(gòu)就是最優(yōu)解。如果業(yè)務(wù)換成高實(shí)時(shí)性、高吞吐、模塊完全獨(dú)立那就應(yīng)該考慮別的方案。3.2 核心組件設(shè)計(jì)與數(shù)據(jù)模型倉(cāng)庫(kù)架構(gòu)落地的第一步是設(shè)計(jì)中央倉(cāng)庫(kù)。我們采用拆分主題域的方式在同一個(gè)數(shù)據(jù)庫(kù)實(shí)例里分多個(gè)業(yè)務(wù)庫(kù)訂單庫(kù)、商品庫(kù)、庫(kù)存庫(kù)、會(huì)員庫(kù)。這仍然是倉(cāng)庫(kù)架構(gòu)因?yàn)槲锢砩纤袔?kù)都在同一個(gè)數(shù)據(jù)庫(kù)集群邏輯上由統(tǒng)一的倉(cāng)庫(kù)訪問(wèn)層對(duì)外提供服務(wù)而不是讓各個(gè)組件獨(dú)立建庫(kù)、私下通信。表結(jié)構(gòu)設(shè)計(jì)我們參考了經(jīng)典范式但特意增加了數(shù)據(jù)契約約束。訂單表的核心字段如下CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id BIGINT NOT NULL, product_id BIGINT NOT NULL, quantity INT NOT NULL, status TINYINT NOT NULL COMMENT 1-待支付2-已支付3-已發(fā)貨4-已完成5-已取消, version INT DEFAULT 0, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, KEY idx_user (user_id), KEY idx_product (product_id) );庫(kù)存表的扣減字段是這樣設(shè)計(jì)的CREATE TABLE inventory ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, total_stock INT NOT NULL DEFAULT 0, locked_stock INT NOT NULL DEFAULT 0, available_stock INT NOT NULL DEFAULT 0, version INT DEFAULT 0, updated_at DATETIME NOT NULL, UNIQUE KEY uk_product (product_id) );這里特意單列了一個(gè)available_stock配合事務(wù)鎖來(lái)實(shí)現(xiàn)安全扣減。數(shù)據(jù)模型層面我們把核心共享數(shù)據(jù)和各組件私有數(shù)據(jù)做了嚴(yán)格區(qū)分。共享數(shù)據(jù)必須進(jìn)入中央倉(cāng)庫(kù)定義全局?jǐn)?shù)據(jù)契約組件內(nèi)部狀態(tài)數(shù)據(jù)和臨時(shí)數(shù)據(jù)允許存在組件本地不進(jìn)倉(cāng)庫(kù)。這個(gè)區(qū)分非常關(guān)鍵它避免了倉(cāng)庫(kù)變成一個(gè)大雜燴清晰的數(shù)據(jù)邊界會(huì)讓后續(xù)遷移和權(quán)限控制輕松很多。3.3 倉(cāng)庫(kù)訪問(wèn)層與操作契約設(shè)計(jì)倉(cāng)庫(kù)架構(gòu)的組件之間不直接通信但不意味著每個(gè)組件都可以直接裸寫SQL訪問(wèn)數(shù)據(jù)庫(kù)。如果不做一層封裝表結(jié)構(gòu)變更會(huì)直接崩到業(yè)務(wù)代碼里這是我們最不愿意看到的。實(shí)踐中我們?cè)谒薪M件和數(shù)據(jù)庫(kù)之間加了一層倉(cāng)庫(kù)訪問(wèn)層也叫數(shù)據(jù)訪問(wèn)層或Repository層。這個(gè)訪問(wèn)層做的事情有三個(gè)統(tǒng)一CRUD入口、統(tǒng)一事務(wù)邊界管理、統(tǒng)一數(shù)據(jù)契約映射。組件調(diào)用時(shí)只能通過(guò)訪問(wèn)層暴露的方法操作數(shù)據(jù)不允許直接寫SQL、不允許跨表裸查。比如創(chuàng)建訂單這個(gè)操作底層實(shí)現(xiàn)大致是Transactional public void createOrderWithStockCheck(CreateOrderRequest request) { // 1. 鎖住庫(kù)存記錄防止并發(fā)超賣 Inventory inv inventoryRepository.lockByProductId(request.getProductId()); if (inv.getAvailableStock() request.getQuantity()) { throw new StockNotEnoughException(); } // 2. 扣減可用庫(kù)存 inventoryRepository.decreaseAvailableStock(request.getProductId(), request.getQuantity()); // 3. 創(chuàng)建訂單記錄 Order order Order.create(request); orderRepository.save(order); }這里有一個(gè)很容易被忽略的細(xì)節(jié)事務(wù)邊界和鎖的順序。所有倉(cāng)庫(kù)操作必須在同一個(gè)事務(wù)里執(zhí)行并且加鎖操作要放在事務(wù)的第一位。鎖庫(kù)存后立即扣減這個(gè)把競(jìng)爭(zhēng)窗口壓到最小如果把查詢放在鎖前面就可能出現(xiàn)檢查完庫(kù)存、還沒(méi)扣減、別的請(qǐng)求已經(jīng)把貨搶走的情況最終導(dǎo)致超賣。4. 落地細(xì)節(jié)與關(guān)鍵實(shí)現(xiàn)并發(fā)、事務(wù)與緩存4.1 倉(cāng)庫(kù)節(jié)點(diǎn)的并發(fā)控制與防超賣實(shí)現(xiàn)倉(cāng)庫(kù)架構(gòu)落地最頭疼的現(xiàn)實(shí)問(wèn)題就是并發(fā)。訂單系統(tǒng)里最典型的場(chǎng)景就是搶購(gòu)下單幾百上千個(gè)請(qǐng)求同時(shí)打到一個(gè)商品上庫(kù)存就那么多怎么確保不超賣且性能不崩我們用了兩層方案。第一層是數(shù)據(jù)庫(kù)行鎖扣庫(kù)存的SQL里加條件判斷不是先查再改而是直接用原子更新UPDATE inventory SET available_stock available_stock - 1, locked_stock locked_stock 1, version version 1 WHERE product_id ? AND available_stock 0;這種寫法的好處是檢查與修改合并為一步數(shù)據(jù)庫(kù)在鎖行之后重新校驗(yàn)條件行鎖天然保證了并發(fā)安全。哪怕同時(shí)來(lái)了500個(gè)請(qǐng)求也只有一個(gè)能把a(bǔ)vailable_stock扣到正數(shù)其余請(qǐng)求全部影響行數(shù)為0我們?cè)诖a里判斷影響行數(shù)為0就提示庫(kù)存不足。第二層是版本號(hào)做樂(lè)觀鎖保護(hù)。數(shù)據(jù)訪問(wèn)層里更新操作都帶一個(gè)version字段更新時(shí)帶上舊版本號(hào)如果版本號(hào)不匹配就說(shuō)明數(shù)據(jù)被別的事務(wù)改過(guò)了要么重試要么失敗返回。這兩層疊加后倉(cāng)庫(kù)并行讀寫的一致性問(wèn)題基本兜住了。事務(wù)隔離級(jí)別我也專門調(diào)過(guò)。創(chuàng)建訂單走的寫事務(wù)使用默認(rèn)的REPEATABLE READ而查詢類接口走只讀事務(wù)用READ COMMITTED減少鎖的持有時(shí)間。隔離級(jí)別不能一刀切全用最高級(jí)因?yàn)楦綦x級(jí)別越高鎖競(jìng)爭(zhēng)越嚴(yán)重吞吐越低。4.2 緩存策略與倉(cāng)庫(kù)性能優(yōu)化倉(cāng)庫(kù)節(jié)點(diǎn)是單點(diǎn)瓶頸所以性能優(yōu)化是倉(cāng)庫(kù)架構(gòu)的長(zhǎng)期課題。我們的核心策略就一條能不進(jìn)倉(cāng)庫(kù)的請(qǐng)求堅(jiān)決不進(jìn)倉(cāng)庫(kù)。具體落地就是分層緩存。熱點(diǎn)數(shù)據(jù)比如商品詳情、庫(kù)存余量、會(huì)員等級(jí)先走本地緩存或分布式緩存緩存沒(méi)命中才落到倉(cāng)庫(kù)。讀多寫少的數(shù)據(jù)用Cache-Aside模式先讀緩存緩存沒(méi)有再讀倉(cāng)庫(kù)讀到后寫回緩存更新數(shù)據(jù)時(shí)先寫倉(cāng)庫(kù)再刪除緩存。這里有個(gè)經(jīng)典坑更新順序搞反。如果先刪緩存再寫數(shù)據(jù)庫(kù)一旦寫庫(kù)失敗緩存就涼了下一次請(qǐng)求會(huì)全部打到倉(cāng)庫(kù)引起緩存穿透。所以我們定的規(guī)則是更新時(shí)先操作數(shù)據(jù)庫(kù)成功后再刪緩存。為了避免并發(fā)場(chǎng)景下緩存和數(shù)據(jù)庫(kù)不一致實(shí)際項(xiàng)目里還用了延遲雙刪先刪緩存再寫庫(kù)然后再刪一次緩存。這個(gè)再刪可能有點(diǎn)多余的系統(tǒng)開(kāi)銷但能顯著降低臟讀概率為了數(shù)據(jù)一致性這個(gè)成本值得付。數(shù)據(jù)庫(kù)層面的性能優(yōu)化我比較看重索引設(shè)計(jì)。查詢多按order_no、user_id維度來(lái)做都建了唯一索引或聯(lián)合索引排序字段始終保持temporal模型直接用created_at做索引避免文件排序。慢查詢?nèi)罩久刻於伎匆槐榭吹阶呷頀叩腟QL就優(yōu)化索引這個(gè)習(xí)慣堅(jiān)持下來(lái)倉(cāng)庫(kù)的負(fù)載會(huì)一直平穩(wěn)。4.3 監(jiān)控與運(yùn)維要點(diǎn)倉(cāng)庫(kù)架構(gòu)的運(yùn)維重點(diǎn)只有一個(gè)盯死倉(cāng)庫(kù)節(jié)點(diǎn)。我們?cè)诒O(jiān)控面板上固定的看四個(gè)指標(biāo)數(shù)據(jù)庫(kù)QPS、平均慢查詢時(shí)長(zhǎng)、鎖等待數(shù)和連接池使用率。連接池使用率是最容易被忽略的。組件多了以后每個(gè)應(yīng)用服務(wù)都會(huì)占一批數(shù)據(jù)庫(kù)連接連接池配置太大會(huì)拖垮數(shù)據(jù)庫(kù)配置太小又會(huì)在流量高峰時(shí)大量報(bào)獲取連接超時(shí)。我們最終把連接池上限根據(jù)倉(cāng)庫(kù)規(guī)格算了一個(gè)安全值數(shù)據(jù)庫(kù)最大連接數(shù)除以應(yīng)用實(shí)例數(shù)再留出30%冗余這樣既不會(huì)被單一服務(wù)打死也不會(huì)白白空占資源。備份策略上倉(cāng)庫(kù)架構(gòu)有一個(gè)天然優(yōu)勢(shì)數(shù)據(jù)集中一套備份機(jī)制全系統(tǒng)都覆蓋。我們每天做一次全量物理備份每小時(shí)做一次binlog增量備份同時(shí)定期做恢復(fù)演練?;謴?fù)演練這個(gè)東西很多人都覺(jué)得沒(méi)必要真到出故障那天才發(fā)現(xiàn)備份文件是壞的或者恢復(fù)流程根本走不通。多演練幾次心里的底氣和系統(tǒng)的高可用能力都會(huì)明顯提升。5. 實(shí)戰(zhàn)中繞不開(kāi)的那些坑常見(jiàn)問(wèn)題與排查實(shí)錄5.1 高頻問(wèn)題速查表問(wèn)題現(xiàn)象可能原因排查思路推薦處理方案接口響應(yīng)越來(lái)越慢缺少索引、緩存命中率低、SQL掃描行數(shù)過(guò)大看慢查詢?nèi)罩?、?zhí)行EXPLAIN分析執(zhí)行計(jì)劃補(bǔ)聯(lián)合索引優(yōu)化SQL增加緩存同一個(gè)用戶重復(fù)下單缺少唯一約束、接口重試機(jī)制不完善查訂單表是否存在重復(fù)記錄、查看請(qǐng)求日志對(duì)order_no等業(yè)務(wù)標(biāo)識(shí)建唯一索引接口加冪等表并發(fā)下庫(kù)存扣成負(fù)數(shù)先查后更新、事務(wù)間隙存在競(jìng)態(tài)查看并發(fā)壓測(cè)日志、觀察鎖等待改成原子UPDATE條件扣減、加版本號(hào)樂(lè)觀鎖數(shù)據(jù)庫(kù)連接耗盡連接池配置過(guò)大、業(yè)務(wù)存在長(zhǎng)事務(wù)監(jiān)控活躍連接數(shù)、打印事務(wù)執(zhí)行時(shí)長(zhǎng)調(diào)小連接池上限拆分長(zhǎng)事務(wù)為短事務(wù)緩存和數(shù)據(jù)庫(kù)數(shù)據(jù)不一致緩存更新時(shí)序錯(cuò)誤、并發(fā)寫對(duì)比緩存值和庫(kù)值、檢查更新代碼順序改為先寫庫(kù)再刪緩存必要時(shí)延遲雙刪倉(cāng)庫(kù)節(jié)點(diǎn)CPU飆高大批量慢SQL、熱點(diǎn)數(shù)據(jù)集中訪問(wèn)查看數(shù)據(jù)庫(kù)實(shí)時(shí)會(huì)話、定位慢SQL優(yōu)化索引、熱點(diǎn)數(shù)據(jù)分流、加緩存這張表是我整理自多個(gè)項(xiàng)目踩坑后的核心沉淀。它最實(shí)用的點(diǎn)在于第三列排查思路很多人遇到問(wèn)題直接上來(lái)改代碼但其實(shí)先定位是哪個(gè)環(huán)節(jié)能省下大半時(shí)間。5.2 三個(gè)我自己踩過(guò)的坑第一個(gè)坑是緩存穿透。早期我們有個(gè)查詢商品詳情的接口大量請(qǐng)求查的商品id根本不存在緩存沒(méi)有、數(shù)據(jù)庫(kù)也沒(méi)有每次都打到倉(cāng)庫(kù)數(shù)據(jù)庫(kù)壓力直接飆高。當(dāng)時(shí)只想到加緩存沒(méi)想過(guò)不存在這種情況也在緩存里做個(gè)標(biāo)記。后來(lái)在緩存里對(duì)不存在的數(shù)據(jù)也緩存一個(gè)空值過(guò)期時(shí)間設(shè)置到五分鐘再配合布隆過(guò)濾器做前置過(guò)濾倉(cāng)庫(kù)壓力立馬降下來(lái)了。第二個(gè)坑是字段變更引發(fā)連環(huán)修改。我們?cè)?jīng)為了支持新業(yè)務(wù)給訂單表加了一個(gè)字段本以為是普通升級(jí)。結(jié)果上線后下游有六個(gè)組件的讀寫邏輯都因?yàn)閿?shù)據(jù)契約變化需要調(diào)整還有一處因?yàn)樽侄雾樞騿?wèn)題導(dǎo)致數(shù)據(jù)映射錯(cuò)亂。從那以后我們強(qiáng)制要求數(shù)據(jù)結(jié)構(gòu)變更走數(shù)據(jù)契約評(píng)審流程任何表結(jié)構(gòu)變化都要先列清楚影響面變更期間做雙寫兼容、灰度切換寧可多花兩天時(shí)間也不能讓倉(cāng)庫(kù)的一個(gè)字段變更炸了整個(gè)系統(tǒng)。第三個(gè)坑是熱點(diǎn)庫(kù)存的鎖競(jìng)爭(zhēng)。某促銷活動(dòng)商品同一時(shí)間幾萬(wàn)人在搶所有扣減請(qǐng)求都鎖同一行庫(kù)存記錄數(shù)據(jù)庫(kù)鎖等待時(shí)間飆到幾百毫秒。后來(lái)我們把單一庫(kù)存拆成多個(gè)子庫(kù)存桶每個(gè)子桶獨(dú)立扣減請(qǐng)求按照用戶標(biāo)識(shí)哈希到不同的桶消除了行鎖熱點(diǎn)。代價(jià)是庫(kù)存合并統(tǒng)計(jì)邏輯變復(fù)雜了一點(diǎn)但在高并發(fā)場(chǎng)景下這個(gè)改法非常有效。5.3 排查思路與一個(gè)實(shí)用小技巧倉(cāng)庫(kù)架構(gòu)問(wèn)題排查我總結(jié)出一條順序先看倉(cāng)庫(kù)側(cè)指標(biāo)再看應(yīng)用側(cè)日志最后才看代碼邏輯。這個(gè)順序非常重要。很多時(shí)候問(wèn)題出在數(shù)據(jù)庫(kù)鎖等待或連接池耗盡但你在應(yīng)用日志里翻半天錯(cuò)誤棧只會(huì)看到一堆表面異常。先看監(jiān)控里倉(cāng)庫(kù)的CPU、慢查詢、活躍連接數(shù)可以快速定位是倉(cāng)庫(kù)本身扛不住了還是某個(gè)應(yīng)用把倉(cāng)庫(kù)拖垮了。最后一個(gè)實(shí)用小技巧數(shù)據(jù)庫(kù)層面排查慢查詢時(shí)別只盯著執(zhí)行時(shí)間長(zhǎng)的SQL還要看高頻短SQL。一些執(zhí)行只要幾毫秒的SQL如果每秒被調(diào)用上萬(wàn)次累計(jì)資源消耗反而比一條秒級(jí)慢查詢更可怕。定位高頻短SQL的方法很簡(jiǎn)單在訪問(wèn)層給每個(gè)倉(cāng)庫(kù)操作打上Tag和耗時(shí)統(tǒng)計(jì)按次數(shù)和總耗時(shí)倒序排列高頻消耗的調(diào)用自然就浮出水面了。這個(gè)小改動(dòng)成本很低但對(duì)倉(cāng)庫(kù)架構(gòu)的整體性能調(diào)優(yōu)幫助是巨大的。我在實(shí)際項(xiàng)目中感受最深的是任何架構(gòu)風(fēng)格都不是一道選擇題而是一道權(quán)衡題。倉(cāng)庫(kù)架構(gòu)帶來(lái)的集中管理和一致性同時(shí)也要求團(tuán)隊(duì)在數(shù)據(jù)模型治理、緩存策略和并發(fā)控制上投入足夠的精力。而一旦你把這些問(wèn)題處理順了這套架構(gòu)的穩(wěn)定性和維護(hù)體驗(yàn)真的會(huì)讓你省心很多。如果你正在做一個(gè)需要多方模塊共享同一份真實(shí)數(shù)據(jù)的系統(tǒng)不妨把倉(cāng)庫(kù)架構(gòu)風(fēng)格認(rèn)真考慮一遍它大概率能讓你少走彎路也更容易把系統(tǒng)做成一個(gè)能長(zhǎng)期跑的業(yè)務(wù)底座。