1. 程式人生 > >JAVA架構師大型分布式高並發電商項目實戰,性能優化,集群,億級高並發,web安全,緩存架構實戰

JAVA架構師大型分布式高並發電商項目實戰,性能優化,集群,億級高並發,web安全,緩存架構實戰

調用 希望 lec nta 微信 只有一個 常見問題 字段 屬性。

現任58到家技術委員會主席,高級技術總監,負責企業,支付,營銷、客戶關系等多個後端業務部門。本質,技術人一枚。互聯網架構技術專家,“架構師之路”公眾號作者。曾任百度高級工程師,58同城高級架構師,58同城技術委員會主席,58同城C2C技術部負責人。

內容介紹

1.大數據量時,數據庫架構設計原則

2.數據庫水平切分架構設計方向

3.用戶中心,帖子中心,好友中心,訂單中心水平切分架構實踐

下面是58沈劍老師的演講實錄

大家好,我是58沈劍,架構師之路的小編,後端程序員一枚,平時比較喜歡寫寫文字。今天和大家分享,數據量很大的情況下,如何進行數據庫架構設計(主要是水平切分)會舉用戶中心,帖子中心,訂單中心的一些例子,希望大家有收獲。

首先,介紹數據庫架構設計中的一些基本概念,常見問題以及對應解決方案,為了便於讀者理解,將以“用戶中心”數據庫為例,講解數據庫架構設計的常見玩法。

第一個概念是“單庫”。

技術分享圖片

user-service:用戶中心服務,對調用者提供友好的RPC接口,user-db:單庫(就是一個庫)進行數據存儲。

第二個概念是“分組”。

什麽是分組?分組架構是最常見的一主多從,主從同步,讀寫分離數據庫架構

技術分享圖片

user-service:依舊是用戶中心服務
user-db-M(master):主庫,提供數據庫寫服務
user-db-S(slave):從庫,提供數據庫讀服務
主和從構成的數據庫集群稱為“組”。分組解決的是“數據庫讀寫高並發量高”問題。

第三個概念是“分片”。

分片架構是大夥常說的水平切分(sharding)數據庫架構。

技術分享圖片

user-db1:水平切分成2份中的第一份,user-db2:水平切分成2份中的第二份,分片後,多個數據庫實例也會構成一個數據庫集群。一旦分片,就涉及分片算法。常見的水平切分算法有“範圍法”和“哈希法”

技術分享圖片

範圍法如上圖:以用戶中心的業務主鍵uid為劃分依據,將數據水平切分到兩個數據庫實例上去。

技術分享圖片

哈希法如上圖
user-db1:存儲uid取模得1的uid數據,user-db2:存儲uid取模得0的uid數據。這兩種分片算法,在互聯網都有使用,其中哈希法使用較為廣泛。

分片解決的是“數據庫數據量大”問題,也就是今天數據庫架構分享的主題。

場景一、用戶中心

第一個案例,先以“用戶中心”為例,介紹“單KEY”類業務,隨著數據量的逐步增大,數據庫性能顯著降低,數據庫水平切分相關的架構實踐。

用戶中心是一個非常常見的業務,主要提供用戶註冊、登錄、信息查詢與修改的服務。其核心元數據為:
User(uid, login_name, passwd, sex, age, nickname, …); uid為用戶ID,主鍵。login_name, passwd, sex, age, nickname, …等用戶屬性。數據庫設計上,一般來說在業務初期,單庫單表就能夠搞定這個需求。

技術分享圖片

當數據量越來越大時,需要多用戶中心進行水平切分,上文提到了“範圍法”與“哈希法”。使用uid來進行水平切分之後,整個用戶中心的業務訪問會遇到什麽問題呢?對於uid屬性上的查詢可以直接路由到庫,對於非uid屬性上的查詢,例如login_name屬性上的查詢,就悲劇了。

技術分享圖片

例如,按照uid分為3個庫,使用login_name=shenjian來查詢,就不知道數據分布在哪個庫上了。一種方法,是遍歷所有庫,當分庫數量多起來,性能會顯著降低。

常見的解決方案,有這麽四種方法:

第一種方法,索引表法

思路:uid能直接定位到庫,login_name不能直接定位到庫,如果通過login_name能查詢到uid,問題解決。

細致的步驟為:

(1)建立一個索引表記錄login_name->uid的映射關系;

(2)用login_name來訪問時,先通過索引表查詢到uid,再定位相應的庫;

(3)索引表屬性較少,只有兩列,可以容納非常多數據,一般不需要分庫

(4)如果數據量過大,可以通過login_name來分庫;

潛在的不足是:多一次數據庫查詢,性能會有所下降。

第二種方法,緩存映射法

思路:訪問索引表性能較低,把映射關系放在緩存裏性能更佳

細致的步驟為:

(1)login_name查詢先到cache中查詢uid,再根據uid定位數據庫;

(2)假設cache miss,采用掃全庫法獲取login_name對應的uid,放入cache;

(3)login_name到uid的映射關系不會變化,映射關系一旦放入緩存,不會更改,無需淘汰,緩存命中率超高;

(4)如果數據量過大,可以通過login_name進行cache水平切分;

潛在的不足是:多了一次cache查詢。

第三種方法,login_name生成uid法

思路:不進行額外查詢,能由login_name直接生成uid麽?

細致的步驟為:

(1)在用戶註冊時,設計函數login_name生成uid,uid=f(login_name),按uid分庫插入數據;

(2)用login_name來訪問時,先通過函數計算出uid,即uid=f(login_name)再來一遍,由uid路由到對應庫;

潛在的不足是:該函數設計需要非常講究技巧,有uid生成沖突風險

第四種方法,基因法(這個方法網上沒有,在“架構是之路”公眾號裏有說明過)
思路:不用login_name生成uid,可以從login_name抽取“基因”,融入uid中。
方法圖示如下(這個圖很重要):

技術分享圖片

假設分8庫,采用uid%8路由。潛臺詞是,uid的最後3個bit決定這條數據落在哪個庫上,這3個bit就是所謂的“基因”。

細致的步驟為:

(1)在用戶註冊時,設計函數login_name生成3bit基因,login_name_gene=f(login_name),如上圖粉色部分;【畫外音,一定要步驟和圖對著看】

(2)同時,生成61bit的全局唯一id,作為用戶的標識,如上圖綠色部分;

(3)接著把3bit的login_name_gene也作為uid的一部分,如上圖屎黃色部分;

(4)生成64bit的uid,由id和login_name_gene拼裝而成,並按照uid分庫插入數據;

(5)用login_name來訪問時,先通過函數由login_name再次復原3bit基因,login_name_gene=f(login_name),通過login_name_gene%8直接定位到庫。如此這般,uid可以直接定位到庫,login_name可以生成基因,也可以定位到庫。

好,用戶中心是第一個場景。

場景二、帖子中心

第二個場景,將以“帖子中心”為例,介紹“1對多”類業務,隨著數據量的逐步增大,數據庫性能顯著降低,數據庫水平切分相關的架構實踐。用戶中心,是一個但key場景,而帖子中心,是一個1對多的場景。

什麽是1對多場景?

一個用戶可以發多條微博,一條微博只有一個發送者;一個uid對應多個msg_id,一個msg_id只對應一個uid;這些是1對多的關系。

技術分享圖片

一個用戶可以發布多個帖子,一個帖子只對應一個發布者。帖子中心,是一個提供帖子發布,修改,刪除,查看,搜索的服務。

讀操作:通過tid查詢帖子實體,單行查詢;通過uid查詢用戶發布過的帖子,列表查詢。帖子檢索,例如通過時間、標題、內容搜索符合條件的帖子。

寫操作:發布(insert)帖子;修改(update)帖子;刪除(delete)帖子。

在數據量較大,並發量較大的時候,通常通過元數據與索引數據分離的架構來滿足實時查詢,以及帖子檢索的入球。

技術分享圖片

架構中的幾個關鍵點
(1)tiezi-center服務;
(2)tiezi-db:提供元數據存儲;
(3)tiezi-search搜索服務;
(4)tiezi-index:提供索引數據存儲;
(5)MQ:tiezi-center與tiezi-search通訊媒介,一般不直接使用RPC調用,而是通過MQ對兩個子系統解耦;

【畫外音:12345對著圖細看一下】

技術分享圖片

如上圖所示:tid和uid上的查詢需求,可以由tiezi-center從元數據讀取並返回,其他檢索需求,可以由tiezi-search從索引數據檢索並返回,tiezi-search可以使用Solr,ES等開源架構實現,這一塊不是今天的重點,今天將重點描述帖子中心元數據這一塊的水平切分設計。在業務初期,單庫就能滿足元數據存儲要求。

技術分享圖片

在相關字段上建立索引,就能滿足相關業務需求,帖子記錄查詢,通過tid查詢,約占讀請求量的90% 。select * from t_tiezi where tid=$tid 帖子列表查詢,通過uid查詢其發布的所有帖子,約占讀請求量的10% ,select * from t_tiezi where uid=$uid。當數據量越來越大時,需要對帖子數據的存儲進行線性擴展,既然是帖子中心,並且帖子記錄查詢量占了總請求的90%,很容易想到通過tid字段取模來進行水平切分。

技術分享圖片

這個方法簡單直接。但缺點是:一個用戶發布的所有帖子可能會落到不同的庫上,10%的請求通過uid來查詢會比較麻煩。

技術分享圖片

一個uid查詢帖子列表,需要遍歷所有庫。有沒有一種切分方法,確保同一個用戶發布的所有帖子都落在同一個庫上,而在查詢一個用戶發布的所有帖子時,不需要去遍歷所有的庫呢?

使用uid來分庫可以解決這個問題。

新增一個索引庫:t_mapping(tid, uid)

(1)這個庫只有兩列,可以承載很多數據;

(2)即使數據量過大,索引庫可以利用tid水平切分;

(3)這類kv形式的索引結構,可以很好的利用cache優化查詢性能;

(4)一旦帖子發布,tid和uid的映射關系就不會發生變化,cache的命中率會非常高;

如此這般,可以保證一個uid的所有tid都在一個庫上,使用tid查詢時,先通過mapping庫查詢到uid,再定位庫,這就是帖子中心場景,使用uid來進行分庫的好處。

mapping表法,和用戶中心的索引表很像,那是不是也能使用“基因法”呢?答案是肯定的,如果login_name生成基因打入uid一樣,可以在uid上取基因,打入tid。

技術分享圖片

如上圖所示,假設分為16庫,用uid%16分庫,假設uid=666的用戶發布了一條帖子

(1)使用uid%16分庫,決定這行數據要插入到哪個庫中;

(2)%16,即分庫基因是uid的最後4個bit,即1010;

(3)在生成tid時,先使用一種分布式ID生成算法生成前60bit(上圖中綠色部分);

(4)將分庫基因加入到tid的最後4個bit(上圖中粉色部分),拼裝成最終的64bit帖子tid(上圖中藍色部分);

【畫外音,對照上圖看1234】

通過這種方法保證,同一個用戶發布的所有帖子的tid,都落在同一個庫上,tid的最後4個bit都相同

於是,通過uid%16能夠定位到庫,通過tid%16也能定位到庫,基因法很有意思,網上幾乎沒有文章介紹,更詳細的基因法介紹,可以掃下列二維碼查閱。

技術分享圖片

沒錯,就是架構師之路,基因法,哈哈。

場景三、好友中心

第三個場景,是好友中心,好友中心,是一個多對多的場景。

什麽是多對多關系?

所謂的“多對多”,來自數據庫設計中的“實體-關系”ER模型,用來描述實體之間的關聯關系。一個學生可以選修多個課程,一個課程可以被多個學生選修,這裏學生與課程時間的關系,就是多對多關系。

技術分享圖片

好友中心是一個典型的多對多業務,一個用戶可以關註多個好友,也可以被多個好友關註。

技術分享圖片

friend-service:好友中心服務,對調用者提供友好的RPC接口,guanzhu表,用戶記錄uid所有關註用戶guanzhu_uid。fensi表,用來記錄uid所有粉絲用戶fensi_uid。一條好友關系的產生,會產生兩條記錄,一條關註記錄,一條粉絲記錄。數據量大時,如何進行水平切分呢?關註表,使用uid分庫,存儲的是關註的人。粉絲表,也使用uid分庫,存儲的是粉絲。由於一條好友關系的產生,會產生兩條記錄,分庫的時候要註意,需要保證數據的一致性,關註庫,粉絲庫,可能存儲在不同的數據實例上,數據的插入難以保證原子性。

這是一個很難的“分布式事務”的問題。具體的數據冗余方式,常見的有這麽兩種:
第一種,同步冗余。

技術分享圖片

顧名思義,由好友中心服務同步寫冗余數據。如上圖1-4流程
(1)業務方調用服務,新增好友關系數據;
(2)服務先插入T1數據;
(3)服務再插入T2數據;
(4)服務返回業務方新增數據成功;

第二種,異步冗余

技術分享圖片

服務層異步發出一個消息,通過消息總線發送給一個專門的數據復制服務來寫入冗余數據。如上圖1-6流程
(1)業務方調用服務,新增數據;
(2)服務先插入T1數據;
(3)服務向消息總線發送一個異步消息(發出即可,異步不用等返回,通常很快就能完成);
(4)服務返回業務方新增數據成功;
(5)消息總線將消息投遞給數據同步中心;
(6)數據同步中心插入T2數據;
這是兩種很常見的冗余數據的方式。數據的一致性如何保證?如果插入T1數據,T2數據插入失敗呢?需要有一個校驗機制。這裏多提一句,為了保證一致性,架構設計的思路有兩種:

(1)分布式事務,保證強一致;

(2)新增異步校驗機制;

第一個方向,很難,是業界沒有解決的難題。或者說,即使有理論上可行的方案,算法效率也非常非常低,不適合互聯網高並發場景。此時的架構優化方向,並不是完全保證數據的一致,而是盡早的發現不一致,並修復不一致。校驗機制,又有兩種常見的方法。
一種是異步掃描校驗

技術分享圖片

線下啟動一個離線的掃描工具,不停的比對正表T1和反表T2,如果發現數據不一致,就進行補償修復,這個方法是最容易想到的。
一種是實時消息掃描校驗

技術分享圖片

(1)寫入正表T1;
(2)第一步成功後,發送消息msg1;
(3)寫入反表T2;
(4)第二步成功後,發送消息msg2;
正常情況下,msg1和msg2的接收時間應該在3s以內,如果檢測服務在收到msg1後沒有收到msg2,就嘗試檢測數據的一致性,不一致時進行補償修復。第一個方案比較容易,但時效性差,第二個方案比較復雜,但時效好。這裏再強調一下,分布式事務一致性,是我被詢問最多的問題。 無數網友在公眾號下方留言問,分布式事務一致性的問題。

這裏再強調一下方法論。高吞吐互聯網業務,要想完全保證事務一致性很難,常見的實踐是最終一致性 。最終一致性的常見實踐是,盡快找到不一致,並修復數據。

場景四、訂單中心

第四個場景,也是最後一個場景,是最復雜的,訂單中心的分庫。這是一個多key的場景。

Order(oid, buyer_uid, seller_uid, time, money, detail…);為啥叫多key呢

(1)oid為訂單ID,主鍵;

(2)buyer_uid為買家uid;

(3)seller_uid為賣家uid;

看到了吧,訪問模式有多個。隨著訂單量的越來越大,數據庫需要進行水平切分,由於存在多個key上的查詢需求,用哪個字段進行切分,成了需要解決的關鍵技術問題。

如果用oid來切分,buyer_uid和seller_uid上的查詢則需要遍歷多庫,如果用buyer_uid或seller_uid來切分,其他屬性上的查詢則需要遍歷多庫。

思路為,多個維度的查詢較為復雜,對於復雜系統設計,可以逐步簡化。假設沒有seller_uid,訂單中心,假設沒有seller_uid上的查詢需求,而只有oid和buyer_uid上的查詢需求,應該怎麽分庫?

沒錯,沒有seller_uid,就蛻化為一個“1對多”的業務場景,對於“1對多”的業務,水平切分應該使用“基因法”。

再次回顧一下,什麽是分庫基因?通過buyer_uid分庫,假設分為16個庫,采用buyer_uid%16的方式來進行數據庫路由,所謂的模16,其本質是buyer_uid的最後4個bit決定這行數據落在哪個庫上,這4個bit,就是分庫基因。在訂單數據oid生成時,oid末端加入分庫基因,讓同一個buyer_uid下的所有訂單都含有相同基因,落在同一個分庫上。

再次假設,這個場景如果沒有訂單ID的oid呢?假設沒有oid上的查詢需求,而只有buyer_uid和seller_uid上的查詢需求,就蛻化為一個“多對多”的業務場景。對於“多對多”的業務,就和好友中心一樣,水平切分應該使用“數據冗余法”(上面提到的關註庫,粉絲庫)。

訂單中心,該怎麽弄呢?任何復雜難題的解決,都是一個化繁為簡,逐步擊破的過程。對於像訂單中心一樣復雜的“多key”類業務,在數據量較大,需要對數據庫進行水平切分時:

(1)使用“基因法”,解決“1對多”分庫需求:使用buyer_uid分庫,在oid中加入分庫基因,同時滿足oid和buyer_uid上的查詢需求;

(2)使用“數據冗余法”,解決“多對多”分庫需求:使用buyer_uid和seller_uid來分別分庫,冗余數據,滿足buyer_uid和seller_uid上的查詢需求;

(3)訂單中心,oid/buyer_uid/seller_uid同時存在,可以使用上述兩種方案的綜合方案,來解決“多key”業務的數據庫水平切分難題;

今天的分享差不多就到這裏,最後做一個小結

水平切分方式;

範圍法;

哈希法;

用戶側,“建立非uid屬性到uid的映射關系”最佳實踐。索引表法:數據庫中記錄login_name->uid的映射關系。緩存映射法:緩存中記錄login_name->uid的映射關系。生成法:login_name生成uid;基因法:login_name基因融入uid;

帖子側,帖子服務,元數據滿足uid和tid的查詢需求。搜索服務,索引數據滿足復雜搜索尋求。uid切分法,按照uid分庫,同一個用戶發布的帖子落在同一個庫上,需要通過索引表或者緩存來記錄tid與uid的映射關系,通過tid來查詢時,先查到uid,再通過uid定位庫。基因法,按照uid分庫,在生成tid裏加入uid上的分庫基因,保證通過uid和tid都能直接定位到庫。

好友側,數據冗余是一個常見的多對多業務數據水平切分實踐。冗余數據的常見方案有兩種:服務同步冗余,服務異步冗余(通過MQ發消息)。數據冗余會帶來一致性問題,高吞吐互聯網業務,要想完全保證事務一致性很難,常見的實踐是最終一致性。最終一致性的常見實踐是,盡快找到不一致,並修復數據,常見方案有:線下掃描法,實時消息法。

訂單側,任何復雜難題的解決,都是一個化繁為簡,逐步擊破的過程。將“多key”類業務,分解為“1對多”類業務和“多對多”類業務分別解決。使用“基因法”,解決“1對多”分庫需求:使用buyer_uid分庫,在oid中加入分庫基因,同時滿足oid和buyer_uid上的查詢需求。使用“數據冗余法”,解決“多對多”分庫需求:使用buyer_uid和seller_uid來分別分庫,冗余數據,滿足buyer_uid和seller_uid上的查詢需求。oid/buyer_uid/seller_uid同時存在,可以使用上述兩種方案的綜合方案,來解決“多key”業務的數據庫水平切分難題。

最後再多說一句,任何脫離業務的架構設計都是耍流氓,共勉。

今天,僅僅只是展開描述了“水平切分”這一個話題,在數據庫架構設計過程中,除了水平切分,至少還會遇到這樣一些問題:

(1)可用性:不管是主庫實例,還是從庫實例,如果數據庫實例掛了,如何不影響數據的讀和寫;

(2)讀性能:互聯網業務大多是讀多寫少的業務,如果提升數據庫的讀性能是架構設計中必須考慮的問題;(3)一致性:數據一旦冗余,就可能出現一致性問題,如何解決主庫與從庫之間的不一致,如何解決數據庫與緩存之間的不一致,也是需要重點設計的;

(4)擴展性:如何在不停服務的情況下擴充數據表的屬性,實施數據遷移,實施存儲引擎的切換,架構設計上都是十分有講究的;

(5)分布式SQL語句:單庫情況下,所有SQL語句的執行都沒問題問題,一旦實施了水平切分,如何實現SQL的集函數,分頁,非patition key上的查詢都成了大問題;

上面這些問題,由於時間的關系,今天不能再展開。要想了解細節,你懂的,掃描上面的二維碼,微信關註“架構師之路”,有你想要的答案。對於“數據庫水平切分”,希望大家有收獲,希望下次還有機會在51CTO群裏分享。

以下問題是來自51CTO開發者社群小夥伴們的提問和分享

Q:Java-風-阿裏:老師分布式事務玩過TCC嗎?

A:58沈劍老師:高並發的場景,基本不玩分布式事務,1秒幾十萬次的並發,分布式事務扛不住的。

Q:後端-陳醫生-北京:說的基因法和數據冗余法,不是非常懂,尤其訂單那塊的基因法。請教一個對於分庫算法的問題,在分庫算法都有什麽?

A:58沈劍老師:今天介紹了,範圍法,hash法。hash法,最常見的是取模,網上討論最多的是一致性hash。強烈建議前者 ,取模就行。

閱讀更多

JAVA架構師大型分布式高並發電商項目實戰,性能優化,集群,億級高並發,web安全,緩存架構實戰