上篇以用戶數據表爲例介紹了基本的數據分割方案以及基本的配置方案。但是在2.0時代,這種簡單的列表索引已經遠遠實現起來是問題的,多對多關係將是最常見的關係。現在我們針對web2.0數據中廣泛存在的多對多關係進行闡述和具體行爲判斷,比如一個很簡單的例子,在2.0時代,好友功能是最常被用到的,每個用戶會有很多的好友,同時也會是很多人的好友,那麼這個數據量將會是用戶數的平方的級別。同樣,對於文章標籤,每個文章可以有多個標籤,而每個標籤又可以有多個文章,這又是一個幾何乘積,數據量又會是個天文數字。
傳統的處理方案有兩種,一種是通過SEARCH的方法來實現,一種是通過另建一個索引表,存貯對應的ID以進行存貯。對於第一種方案,因爲要涉及大量的LIKE查詢,性能不敢恭維,第二種的情況下,數據庫的行的數量也是驚人海量級別的,並且要跨表跨區查詢,還要維護數據的唯一性,數據處理過程相當的複雜性能也就不言而喻了。
文入正題,下面對數據多對多關係舉出來具體的解決方案,我們這裏以標籤和文章之間的多對多關係爲例來講解,大家可以舉一反三的思考羣組和用戶之間,相冊和被圈用戶之間等等複雜的多對多關係。
首先濾清一下流程,我們以傳統方案的第二種爲例,在傳統的數據庫設計中我們是如下走的:當一篇博文發佈的時候並插入標籤的時候一般是三步走(也可以理解爲四步,以爲還要判斷標籤是否存在的問題),第一步插入文章數據庫並獲取文章的ID,第二步插入標籤數據庫同時查詢標籤是否存在,如果存在就取出標籤的ID,否則的話插入新標籤並取出ID,第三部,將文章的ID和標籤的ID插入索引表來建立關聯。如果這個時候在索引表上建立了索引的話就是災難性的,特別是在數據量大的情況下,儘管它可以有效的提高查詢速度,但是發佈的速度可能就會讓人無法忍受了。
對文章做冗餘字段,加一個TAG列,我們可以講TAG的標籤如下寫[TagID,TagName]| [TagID,TagName]| [TagID,TagName] 同樣 對於TAG表,我們做如下冗餘加個Article字段,如下內容[ArticleID,Title]| [ArticleID, Title]| [ArticleID, Title],在需要增加的時候我們只要APPEND一下就可以了,至於ARTICLE的結構和TAG的結構可以參考我上一篇文章的介紹。其實根據需要還可以存貯更多。
有人會問,爲什麼要存貯TagName和ArticleTitle呢,其實是爲了避免跨表查詢和INNERJOIN查詢來做的,In查詢和跨表查詢會造成全表遍歷,所以我們在執行的時候In查詢是必須要找到一個有效的替代方法的。
爲了避免文章在發佈的時候以爲要檢查TAG表而造成的線程擁堵,我們需要採取延遲加載的方案來做。服務器應該維護一個進程專業的對標籤和文章地段的查詢和索引,我們在發佈文章的時候應該把標籤同步這一塊託管給另外的一個程序進行處理,並進行索引。
對於頻繁的判斷標籤去或者熱門的標籤我們還可以組織一套有效的索引,比如對於標籤“瘋狂代碼”和”傲博知識庫”,我們用樹來把它表示出來。對於瘋狂代碼我們索引一個瘋,其實用程序表達就是瘋狂代碼[0],同樣傲博知識庫就是傲博知識庫[0]。而在數組”瘋”中存貯以瘋開頭的標籤組,以”傲”的數組中存貯以”傲”開頭的標籤。如果量更大的話還可以再做二級索引。
這涉及另外一個話題了就是分詞,上面是一個簡單的分詞方案,大家在進行GOOGLE搜索的時候應該很輸入它的Suggest方法吧,就是這個道理。最終講標籤有效的索引,並提取熱門的作爲一個全局靜態變量,我們就可以繞過數據查詢這一關,對第二部的單件模式又是一個進化。
OK,咱們可以進一步的把它來抽象化,我們用TableA 表示Article表,用TagbleT表示Tag表,我們可以講字段抽象化出來,也就是一個ID,一個Tag的String 同理對於標籤表也是如此。朋友們應該可以理解我的意思了。
對,就是做個代碼生成器把對應的多對多關係給生成出來,這個很好寫的,幾個Append就可以搞定。如果想更方便的處理,那麼把這個東西做成單件的模式抽象化出來,然後再違反一下原則,做成基類,其他關係繼承這個基類。。。。。剩下的應該很簡單了,具體實現大家思考吧。
上篇以用戶數據表爲例介紹了基本的數據分割方案以及基本的配置方案。但是在2.0時代,這種簡單的列表索引已經遠遠實現起來是問題的,多對多關係將 是最常見的關係。現在我們針對web2.0數據中廣泛存在的多對多關係進行闡述和具體行爲判斷,比如一個很簡單的例子,在2.0時代,好友功能是最常被用 到的,每個用戶會有很多的好友,同時也會是很多人的好友,那麼這個數據量將會是用戶數的平方的級別。同樣,對於文章標籤,每個文章可以有多個標籤,而每個 標籤又可以有多個文章,這又是一個幾何乘積,數據量又會是個天文數字。這裏不再介紹基於硬件,IO,集羣方面的問題,我們以項目開發的角度來實現他
這裏先介紹一個基本的施行方案,而後我們進一步的對它進行擴充以滿足我們的以後的具體需求
對於多對多關係,傳統的處理方案有三種,一種是通過SEARCH的方法來實現,第二一種是通過另建一個索引表,存貯對應的ID以進行存貯,第三種是通過二次歸檔緩衝來實現(本人不知道用什麼語言來描述這種處理方法,姑且如此吧)
對於第一種方案,因爲要涉 及大量的LIKE查詢,性能不敢恭維,基於全文索引的方式可能解決這個問題,但是利用第三方的數據可能未必能適合我們的胃口,我們也可能沒有足夠的時間和精力來獨立開發實現。第二種的情況下,數據庫的行的數量也是驚人海量級別的,維護索引表的散列處理,並且要跨表跨區查詢,還要維護數據的唯一性,數據處理過程相 當的複雜性能也就不言而喻了。
文入正題,下面以一個簡單的例子解釋下第三種方案,對數據多對多關係舉出來具體的解決方案,我們這裏以標籤和文章之間的多對多關係爲例來講解,大家可以舉一反三的思考羣組和用戶之間,相冊和被圈用戶之間等等複雜的多對多關係,如下方案可能不是最好的方案,但是實踐證明還是綜合時間和開發成本是最合理的。
首先濾清一下流程,在傳統的數據庫設計中我們是如下走的:當一篇博文發佈的時候並插入標籤的時候一般是三步走(也 可以理解爲四步,以爲還要判斷標籤是否存在的問題),第一步插入文章數據庫並獲取文章的ID,第二步插入標籤數據庫同時查詢標籤是否存在,如果存在就取出 標籤的ID,否則的話插入新標籤並取出ID,第三部,將文章的ID和標籤的ID插入索引表來建立關聯。如果這個時候在索引表上建立了索引的話就是災難性 的,特別是在數據量大的情況下,儘管它可以有效的提高查詢速度,但是發佈的速度可能就會讓人無法忍受了。
我們處理的方法也是四部曲,對多對多關係進行進一步的處理。
用標籤的時候,我們用的最多的就是查詢標籤下的文章和顯示文章的標籤,所以我們實現這例就成了。
第一步,數據冗餘
老生常談的話題,對文章做冗餘,加一個TAG列,我們可以講TAG的標籤如下寫[TagID,TagName]| [TagID,TagName]| [TagID,TagName] 同樣 對於TAG表,我們做如下冗餘加個Article字段,如下內容[ArticleID,Title]| [ArticleID, Title]| [ArticleID, Title],在需要增加的時候我們只要APPEND一下就可以了,至於ARTICLE的結構和TAG的結構可以參考我上一篇文章的介紹。其實根據需要還 可以存貯更多。
有人會問,爲什麼要存貯TagName和ArticleTitle呢,其實是爲了避免跨表查詢和INNERJOIN查詢來做的,In查詢和跨表查詢會造成全表遍歷,所以我們在執行的時候In查詢是必須要找到一個有效的替代方法的。關於數據冗餘的問題,我們可能還會做的更變態一些,這個後面慢慢說。
第二步:異步存貯。
在設計模式下我們常思考的是單件模式,我們採用另類的單件模式思維來處理,也就是把文章和標籤之間的索引作爲專門的進程來做,異步的實現。
爲了避免文章在發佈的時候以爲要檢查TAG表而造成的線程擁堵,我們需要採取延遲加載的方案來做。服務器應該維護一個進程專業的對標籤和文章地段的查詢和索引,我們在發佈文章的時候應該把標籤同步這一塊託管給另外的一個進程或者服務器進行處理,並進行索引。
第三步:二次索引:
對於頻繁的判斷標籤去或者熱門的標籤我們還可以在內存裏組織一套有效的索引,比如對於標籤“瘋狂代碼”,我們用樹來把它表示出來。對於 瘋狂代碼我們索引一個瘋,其實用程序表達就是瘋狂代碼[0]。而在數組”瘋”中存貯以瘋開頭的標籤組,以”傲”的數 組中存貯以”傲”開頭的標籤。如果量更大的話還可以再做N級索引,將這些常用的標籤對應設計內存索引,我們可以把它想象的理解爲內存中的Suggest(比如google搜索時的Suggest),使用中我們可以直接拿來使用
第四步:針對跨表查詢的處理
很多情況下,我們可能避免不了多表查詢,或者IN,or查詢,除去業務層封裝的分區視圖集羣之外,我們還可以處理的更好,在很多情況下,我們的查詢會是非常頻繁非常統一的(這裏的統一指熱門查詢),比如在SNS中常見的性別,嗜好等多條件搜索,而這些數據可能存貯在多個數據表結構中,而這樣會吧不可避免的會產生全表遍歷查詢。
處理方法也很簡單,把原來散列的垂直分割的表再合併起來,合併到另外的只讀的訂閱服務器上,然後做適當的結構優化和索引,剩下的大家應該明白我的意思了,雖然簡單,但是這種處理方法非常適合以後服務器的橫向擴充。
以上是對多對多關係和多表查詢的一個簡單的架構說明,肯定有人會問,如果這樣做的話工作量不是太大了嗎,分詞處理什麼的,對每個多對多關係進行處理。
OK,咱們可以進一步的把它來抽象化,我們用TableA 表示Article表,用TagbleT表示Tag表,我們可以講字段抽象化出來,也就是一個ID,一個Tag的String 同理對於標籤表也是如此。朋友們應該可以理解我的意思了。
對,就是做個代碼生成器把對應的多對多關係給生成出來,這個很好寫的,幾個Append就可以搞定。如果想更方便的處理,那麼把這個東西做成單件的模式抽象化出來,然後再違反一下原則,做成基類,其他關係繼承這個基類。。。。。剩下的應該很簡單了,具體實現大家思考吧。
讓併發來的更猛烈些吧,高併發環境下的數據處理方案
對於高併發性質的網站,在sns特別是webgame方面應該是最容易也是最難處理的地方了,容易處理的是如果是純粹基於數據庫驅動也就是select和update的問題,而難的地方也是不是select而是update,在高併發的驅動下,update經常會超時,雖然我們可以在finally把它處理掉,讓人鬱悶的是,數據庫連接池仍然會飽和,數據仍然會丟失….
上面的情況是非常常見的web項目失敗的原因之一,在數據飛速膨脹和併發呈幾何級增長的情況下,制約我們的可能是io,database本身的問題了,讓我們頭痛的是不管是哪種數據庫,Oracle也好,mysql也好,sqlserver也好都會timeout,而且是頻繁的timeout頻繁的Exception。這個時候就需要我們的應用程序在處理的前期就應該考慮到的,一個好的數據緩存策略常常決定了我們的成敗,而緩存策略也是web項目最難以測試和最容易出錯的地方。
在大型網站架構中,最關鍵最核心的也是緩存策略了,介於其複雜性,這裏只簡單的介紹一下基於高併發數據庫緩存方案,後面的將詳細介紹常用的緩存策略。這個方法與其叫緩存不如叫數據緩衝,其實也是異步更新數據,根據負載情況不同,我們哪怕僅僅將數據緩衝1秒,帶來的負載提升就已經非常好了。
實現原理很簡單,將併發的更新首先緩存到一個應用程序池中,然後定時查詢(注意這裏的方案應和緩存方案具體結合,這裏只介紹概要情況)。
傳統的update請求處理流程是:請求—》應用程序—》更新數據庫,如下圖:
數據緩衝和更新部分可以在數據層裏獨立實現,也就是update的傳遞的時候首先傳遞緩衝池,然後定時更新,這裏需要注意的數據緩衝池的還要做的另外一份工作就是全局的數據緩存,緩存數據更新到數據這段的時間間隔,我們可以理解爲臨時表,再提取上下文請求的即時信息的時候首先從緩衝池裏讀取(這裏有很多技巧,比如巧妙的利用cookie,session做;臨界條件判斷),流程如下圖所示
上面簡單的介紹了一下基於數據更新緩存的處理,下篇具體詳細介紹基於併發更新機制的詳細緩存處理機制
瘋狂代碼,大型網站架構系列之五,緩存策略設計概要
上篇對瘋狂代碼緩存配置進行了概要的設計,可能說的有點模糊了,有幾個朋友發了幾個問題探討了下,這裏有必要先澄清一個問題,和常見的緩存策略不同,我們的緩存策略將重點放在更新策略而不是隻讀策略上。只讀緩存以及共性緩存策略性質實現的難度並不大,我們要解決的是非共性緩存,併發更新緩存,可擴充性緩存,分佈式緩存更新運算的問題,而對於共性的東西的話我們可以很輕鬆的實現,而不必做太多的運算。
試想一個問題,對於一個多用戶的併發的系統,如果對每個用戶都維護一份緩存策略還要保證更新的及時性以及處理的必要性來說的話,我們很難想到一個有效的處理機制來維護每份(每用戶)緩存的副本的,緩存的存儲性質也決定了做分佈式緩存策略處理的難度和分佈式通訊更新的的難度,我們也很難嘗試對於一些訪問量很小且少有共性的頁面實現有效的緩存命中率,比如某某用戶的博客。
簡單的總結了一下關於緩存策略討論的重點
A. 基於海量非共性數據的緩存策略
B. 基於數據緩存級別併發更新的緩存策略
C. 基於數據併發存儲的緩存策略
D. 基於分佈式的緩存策略
E.基於搜索的緩存策略
我們這裏不再贅談關於頁面靜態化以及類似的問題,靜態化的情況非常適合在系統初期,用戶的基數並不算很大的情況下實現,而在涉及集羣的情況下,靜態化的實現成本,IO成本,維護成本,擴充成本以及更新成本會遠遠的超出緩存策略的成本,當然我們也會有一套建立在緩存基礎上的靜態化處理方案,這些放在以後再談。我們的目的是要建立一個可伸縮,便於維護擴展的緩存策略,下面就具體問題進行分析。
對於問題A:
常見的博客系統就是一個最好的例子,每個用戶的首頁都是相對個性的數據,共性的地方不多,以常見的處理方案來說的話,我們可能需要維護每個用戶訪問的緩存副本,而對於一些訪問量極小的博客站點來說的話這種方式無疑會造成巨大的浪費。
對於大量非共性的數據緩存來說,幾個處理方案:
1) 量化緩存目標並分配相應的緩存權值。(權值分級)
目的很簡單,只緩存有效的數據。首先抽取活躍用戶,以及高訪問量用戶,將數據進行分組分權制緩存(對於交友型的SNS系統來說,我們稱之爲美女效應)
2) 非連接持久性的緩存保持(臨時的持久性)
珍惜並有效利用數據查詢,將未被緩存命中時的查詢或者無權值的數據持久化保存(序列化存貯靜態存貯等),當緩存未被命中時優先取得持久化數據而非數據查詢。可以理解爲臨時數據存貯,或者臨時存貯於子服務器的某個位置。
3) 基於數據更新的緩存清除(一次性使用)
當持久性緩存保持失效(依賴數據發生修改),直接刪除臨時數據(緩存只在訪問時被激活並儲存,一旦修改或者失效,我們立刻拋棄)。
4)緩存更新代理規則
由另外的線程進行維護,並維護線程的有效性,最大限度的分離主程序對無效緩存以及臨時持久性緩存數據的清理
對於問題B:
在小型緩存策略中,緩存處理對於整個應用程序對於每個請求來說都是唯一的,可操作的和非物理存儲的。而在併發更新的過程中,一個小小的併發更新就會很現實的清空所有的緩存池,造成緩存命中率奇低而初始化率奇高而起不到緩存策略應有的作用。
在這種情況下,處理方案也和A.4中提到的方案是一樣的,由獨立的緩存更新進程來處理,對於應用程序中所有涉及緩存更新的請求由專門的更新代理來執行。這個處理方案相對簡單,不再贅述。
對於問題C:
上篇已經提到關於併發數據更新會帶來的問題也就是數據庫的I/O響應,超時,死鎖,以及線程的阻塞問題。我們用一個寫入緩存來處理這個方案,其實這個並非傳統意義上的讀緩存,姑且命名爲寫緩存吧,我們可以形象的理解爲類似硬盤緩衝區的問題。這裏處理的操作稍微有點多了,還要涉及只讀緩存的更新的問題了。
根據系統的不同,我們需要分析處理的角度也不同,我們以常見的webgame爲例來簡單介紹一下處理機制,這裏有兩種常見的情況
1) 對於webgame的最終用戶玩家來說,每個在線用戶的數據是非共性的(問題A),而在一個戰鬥場景下,每組數據時刻都在變化之中,如果我們對數據的變化採用數據庫日誌記錄的形式保存的情況顯然對Database的壓力很大,而我們需要記錄的僅僅是戰鬥的結果,戰鬥的過程我們完全沒有必要進行保存,這個時候我們就用寫入緩存來執行相應的數據操作。這個處理很簡單,用服務器變量的形式就能解決他。
2) 對於webgame的服務器角色來說,如果戰鬥場景的用戶量非常多,而數據更新非常大的情況下,我們採用方法1中的處理也可能力不從心,這個時候我們可以將緩存來進一步的抽象,在某個時間段內(比如3分鐘),維護一個唯一的緩存對象,所有的數據操作都在這個時間段來被緩存進程來記錄,來更新。而由另外的一個進程來進行異步的定時的數據保存操作。
對於問題D
這個是比較常見的分佈式緩存服務器組了,而對緩存服務器來說其實要解決的問題就是服務器間之間互相通訊的問題,並保證數據一致性的問題。那麼我們的有四個處理規則:
1) 數據緩存應該被有效的分組並索引
目標是實現數據耦合的成都降到最低,甚至沒有耦合。比如以用戶ID爲分割的數據緩存分佈,或者以文章分類爲分割的緩存分佈
2) 數據緩存應該被有效的更新
如果數據被有效的分組完成後,這個就是問題C.2的方案了,和C.2不同的是,因爲緩存組可能未必在一組服務器中,可能涉及緩存和DATABASE數據通訊延遲的問題。這個時候要保證緩存服務器被即時的傳遞到databse,那麼需要另外的一個緩存檢測進程來完成這項工作(數據完整性檢查,並備份兩個緩存段的數據)
3) 緩存服務器間的數據完整性
對於無法分組的數據,比如時間段內的用戶認證數據和資料數據,我們需要保證兩組數據同步,最好的處理方法就是清除相應的緩存段,讓它在下次使用的時候初始化
4) 緩存服務器間的連通性
這個取決於物理線路,如果緩存服務器在天南地北的話,我們還需要一個隊列進程來進行同步和數據矯正,我們稱之爲緩存路由。
對於問題E
在分佈式緩存的情況下,多條件搜索往往涉及多個緩存服務器,處理起來筆者尚未有一套完善的出來方案。筆者用的是敷衍原則和集成原則了
敷衍原則:
對於搜索型的數據來說,很多情況下並不是非常重要,我們的搜索結果完全可以晚一會提供給用戶,允許搜索的數據有10分鐘或者更長時間的延遲。
集成原則
將搜索字段和表整合出來,用獨立的只讀查詢服務器來分擔負荷
如果您有比較好的方案,不妨mail:heroqst # gmail.com ,和瘋狂代碼探討下,請替換#爲@。
本文到這裏簡單的介紹了幾種緩存處理的方案,僅供參考。下篇將結合本文的緩存策略探討web 2.0下的數據規劃原則