web應用從單點向高並發架構演變時往往遇到最大的問題就是數據庫的分布式存儲。因為web應用本身就可以集群部署,但其所使用的數據庫確是單點的。如果一個web應用開始的時候沒有考慮數據庫的分布式架構,那么等到要進行數據庫集群改造時會發現困難重重,此時通常的做法是將原系統拆分成多個子系統,然后每個子系統訪問一個數據庫,這幾乎重寫了整個系統(如果這還不能滿足需求,大型企業接下來會增加數據存儲總線)。很多廠商都是這么做的,包括淘寶。如果你開始你能夠考慮到數據庫集群,並且這種集群的設置並不增加開發工作量和難度,那么將來可以為企業節省大量人力物力。
一般來說我們在開發一個應用時,需要對應用進行分層,一般分為界面層、業務層、存儲層,這有利於開發,也有利於將來部署運行。這里並不特指Web程序,桌面應用也是一樣。應用系統從單點運行演變為分布式應用一般需要經歷下面3個步驟。

圖(1),系統的各層屬於一個應用,彼此之間以API的方式(進程內)調用,可以滿足小並發量需求,一般的桌面應用使用此種運行方式。圖(2)系統的業務層被拆分,拆分成許多獨立的子系統(或同一系統的業務處理模塊單獨運行,部署多個),前端界面和業務層以RPC進行調用,業務層和存儲層以API的方式(進程內)調用,此種運行方式可以滿足中、高並發量的處理請求。圖(3)就是分布式的最終狀態了,所有功能都進行集群部署,層與層之間都是RPC調用。
關於分布式,這里不再多說,本文主要討論的是存儲架構的問題。我們知道最簡單的一個Web應用就可以進行圖(2)那樣的部署(如果需要登陸,那么還需采用session共享技術),這是因為Web應用本身界面層和業務層默認就是http調用,屬於遠程調用的一種。我們假設有一個電子商務類應用,運行方式如下圖:

因為Web應用本身界面是由頁面組成,並且可以緩存,我們也可以把界面層看成進行了集群部署。所以說,對於一個簡單的web應用是可以很容易做到界面層和業務處理層集群部署和運行的。那么隨着業務量增大和並發請求的增加,一個web應用最先遇到的瓶頸就是存儲。從開發角度最容易看出這個問題,一個默認web應用,在訪問數據庫時都是使用同一條數據庫連接(或同一個連接池),只能指向同一個數據庫。所以,如果我們能夠在一個web應用中,讓業務層按需建立多個數據庫連接,那么也就實現了存儲的集群化。
一般來說一個應用遇到存儲瓶頸,可以通過緩存進行優化,如果緩存也不能滿足情況,就只能拆分數據庫了。拆分數據庫一般分為兩步走,首先是數據庫按業務垂直拆分,不要業務模塊的數據存儲到不同的數據庫,這也稱為數據庫的垂直拆分,如下圖(1)。這是指同一個應用來說的,微服務架構本身包括多種應用,每種應用有自己的數據庫訪問模塊,每種應用又可以包括多個業務處理模塊,垂直拆分后,每個業務模塊對應一個數據庫。
因為微服務架構本身就提倡將大應用拆分成多個子服務,每個應用有自己的數據庫,這已經相當於進行了一次存儲的垂直拆分。再進一步垂直拆分,一個子服務又可以使用多個數據庫,到這一步,一般的高並發量就足夠處理了。但是一個系統往往有些模塊會持續不斷的增加數據,比如電子商務中的訂單模塊,這會導致單表數據非常大。時間久會給查詢和計算帶來嚴重瓶頸,這個時候就需要數據庫水平拆分登場了,如圖(2),數據庫水平拆分是把之前某個業務處理模塊對應的一個數據庫,再按水平方向拆分,使一個業務處理模塊對應操作幾個數據庫,使原來一張表中的數據按規則存儲到幾個庫中。

水平拆分的前提條件是拆分的相關表必須屬於同一個聚合(領域驅動中的聚合)。聚合中的根就是拆分依據的主表,我們根據主表數據計算規則進行數據庫拆分。這種拆分方式非常重要,因為這種拆分方式形成的數據庫橫向集群支持數據庫級事務。比如說一個用戶聚合,包括用戶、資金賬戶、聯系人3張表。那么用戶就是主表,此時如果我們按用戶ID拆分數據庫,你會發現雖然是集群存儲,但當某個用戶訪問自己的數據時,這些數據始終在同一個庫,同一個庫就支持數據庫事務。
做完橫向拆分后,每一個服務其實都對應了一個存儲矩陣,服務中每一個業務處理模塊都對應幾個數據庫,這幾個數據庫是水平切分的。最簡單的水平切分規則是按主表主鍵大小,比如前100萬條數據存A1數據庫,100萬-200萬存A2數據庫,200萬-300萬存A3數據庫等。

數據庫存儲集群的方案目前業界有很多。關於關系型數據庫和NoSQL使用場景也是爭論不休。我們認為,如果是事務性的應用必須使用關系型數據庫,比如電子商務和互聯網金融,事務性比較弱的可以采用NoSQL,比如博客、新聞等應用,事務性可以作為是否選用NoSQL數據庫的標准。即便是關系型數據庫集群在編程方面也存在多種方案,比如:

這個技術都各有優缺點,有的不支持本地數據庫級事務,像Amoeba;有的支持數據庫本地事務但又不支持跨庫情況下的Join、分頁、排序、子查詢,像Cobar。從本質上說,數據存儲也是業務處理的一部分,舉個簡單的例子,還是那個轉賬的例子:A在Web頁面提交一個轉賬給B申請,業務處理模塊拿到轉賬申請從A賬戶扣除轉賬金額(數據庫操作)並給B的余額加上轉賬金額(數據庫操作),並記錄操作日志和通知B。很明顯,數據庫操作是這個轉賬業務處理操作的一部分,所以如果在業務層直接進行分布式存儲和按需存儲,事情將變得簡單的多(spring提倡的鏈式事務管理就是這種方式)。如果你不把數據庫存儲作為業務層處理的一部分,而封裝成一個獨立運行的模塊,那么就會出現各種各樣的問題。
如果將數據庫路由鍵放入服務層,一個典型業務層接口類定義如下:
public interface SporeService {
public List<Spore> searchByCondition(RouteKey routeKey);
public Spore findById(Long id,RouteKey routeKey);
public void update(Spore spore,RouteKey routeKey);
public void insert(Spore spore,RouteKey routeKey);
public void delete(Long id,RouteKey routeKey);
}
這是業務層定義的接口,在這里增加了一個數據路由的key,為什么把數據路由的選擇放到業務層接口,我們前面提到過,數據訪問本身屬於業務的一種操作。就比如你今天去A銀行取錢和去B銀行取錢完全是兩種業務。數據路由鍵讓每一個業務方法都可以對應一個數據庫矩陣,而key就是該業務方法所需數據庫的選擇方法。可能有的程序員認為在每個方法增加一個RouteKey routeKey參數太過於繁瑣,是不是可以定義成繼承關系或注解等等,那相比這種方式更加繁瑣,而且不適合於所有情況。
