1. 程式人生 > >【轉載】我也簡單談下《Web應用的快取設計模式》

【轉載】我也簡單談下《Web應用的快取設計模式》

拜讀了Robbin的文章《Web應用的快取設計模式》http://robbinfan.com/blog/38/orm-cache-sumup ,我覺得大體思想還是值得學習和借鑑的,借這機會順便簡單談談我一般的做法,基於它文章Blog的例子和場景。

以讀取部落格文章列表和文章為例

一、資料庫設計

首先,從資料庫設計上,我贊同Contents拆分出去,在顯示列表時,是沒必要讀取完整內容的。但如果快取應用得當,這個可以屬於可選項,並非必須。按照我的習慣,表設計會如下:

Blogs表,用以儲存部落格內容
BlogId int 用以儲存部落格內容,表主鍵,聚集索引
Title nvarchar(256) 部落格標題
Content nvarchar(MAX) 部落格內容
FormattedContent nvarchar(MAX) 格式化後部落格內容,空間換時間,沒必要消耗CPU去格式化markdown。可選項,也可以運算後放快取
AuthorId   int 和Accounts表關聯
Author nvarchar(256) 作者,冗餘欄位,可以不必查詢Accounts表
BlogDate datetime 部落格釋出時間

補充說明:

1. 適當冗餘,例如FormattedContent和Author欄位,減少跨表查詢或CPU運算
2. Content和FormattedContent欄位可以考慮分離到另一個表縱向拆分,但並非必要選項,這裡我沒有分開,儘可能在一起
3. 該表可以用非關係資料庫如Key-Value資料庫儲存,當資料增大可以根據BlogId進行合理分割槽,這樣可以避免單表過大影響查詢效率。對WindowsAzure瞭解的可參考TableStorage。

光這個表,部落格的內容是都有了,但要達到比較高的效率,我還會根據業務場景建立幾個“索引表”,舉兩個業務場景:1,首頁分頁瀏覽部落格文章列表 2,根據Tag檢索部落格文章,這兩個場景會對應兩個“索引表”,如下所示:

BlogList表,儲存部落格列表資訊
BlogId int 部落格Id,和Blogs表的BlogId關聯,主鍵,非聚集索引
BlogDate   datetime 部落格釋出時間,和Blogs表的BlogDate一致,聚集索引
TagBlogs表,紀錄Tag和Blog的對應關係
Tag   nvarchar(64) Tag欄位。和BlogId欄位形成雙主鍵,非聚集索引
BlogId int 部落格Id,和Blogs表的BlogId關聯
BlogDate datetime 部落格釋出時間,和Blogs表的BlogDate一致。Tag和BlogDate作為聚集索引

補充說明:
1. 所謂索引表,其實功能和索引類似的,就是根據查詢條件查出來實體物件主鍵(BlogId)。
2. 聚集索引儘量建立在查詢條件上,以儘可能提高檢索效率
3. 對於效能要求高的查詢單獨索引表
4. 對索引表的查詢結果為滿足條件主鍵集合,例如:“select BlogId from BlogList order by BlogDate desc” or "select BlogId from TagBlogs where Tag = @Tag order by BlogDate desc"

表設計基本如此

二、資料庫查詢

1. 根據id集合返回實體集合,不提供單個id查詢。儲存過程應該有很多種寫法,類似於 “select BlogId, Title, Content, FormattedContent, AuthorId, Author, BlogDate from Blogs where BlogId in (2, 4, 7, 12, 88)”
2. 根據業務場景去查id集合,例如:“select BlogId from BlogList order by BlogDate desc” or "select BlogId from TagBlogs where Tag = @Tag order by BlogDate desc"
3. 涉及分頁在記憶體中完成。例如我要按照時間倒序查所有部落格列表,那麼根據上面的Sql會得到一個排序好的BlogId集合,轉換成記憶體的一個數組,例如:[100,99,98,97,96....5,4,3,2,1],如果我要現在每頁5條紀錄,那麼第一頁的集合就是:[100,99,98,97,96]。然後根據把它作為一個id集合,就可以獲取到一個實體集合

這樣把資料庫查詢分成兩次,確實有脫褲子放屁之嫌,但結合快取來看就不會了。

三、快取

快取的粒度確實是個非常重要的問題,舉例來說,如果按照“SELECT * FROM blogs ORDER BY id DESC LIMIT 20”來查詢一頁部落格資料集合。並將它快取到記憶體中,那麼如果部落格更新頻繁(象部落格園這樣的頻度),那麼快取命中率極低,所以我一般會把快取分成兩級。
1. 第一級,id->entity,也就是粒度為單個實體,舉部落格的例子,就是Key是blogid,Value就是Blog實體物件。這個需要有一個Cache更新、清理的邏輯,有很多成熟方案,不贅述。
2. 第二級,查詢條件->ids,也就是查詢條件到id集合的一個快取,舉例來說,根據Tag查詢部落格,那麼我會把Tag作為Key,Value則為該Tag下按照時間倒序排列的所有BlogId的集合。這個根據資料更新頻度,一般Cache時間很短,這樣可以保證資料更新了快取也會及時更新,並且它都是在索引表查詢,資料庫查詢效率極高。

根據上面二級快取的設計,快取命中率會大大提高,例如即使帖子列表更新,一級快取也不會實效。二級快取其實只是輔助,但是由於索引表的存在,不用擔心查詢效率降低對快取的依賴。

四、綜合

綜合以上種種,一個結合快取的資料查詢會如下過程(以按照時間倒序分頁瀏覽部落格列表為例):

1. 首先根據業務條件去快取查詢id集合,如果快取沒有,就去資料庫查詢,並將查詢結果更新到快取。假如我獲得的id集合為:[100,99,98,97,96....5,4,3,2,1]
2. 根據分頁條件,在記憶體中從返回的所有id集合中獲取頁id集合,假如頁id集合是[100,99,98,97,96]
3. 根據頁id集合中的id,去一級快取中逐一檢查快取是否有資料,過濾掉快取中有的。例如[100,99,98,97,96]中[100,97,96]三個在Cache中已經存在,那麼留下[99,98]
4. 用過濾後的id集合(如果為空跳過該步驟)去資料庫中獲取實體集合,並將結果加入一級快取。例如“select BlogId, Title, Content, FormattedContent, AuthorId, Author, BlogDate from Blogs where BlogId in (99,98)”,並將它加入快取
5. 根據id結合順序拼合實體集合返回,例如:[blog100,blog99,blog98,blog87,blog96]
6. 如果分頁控制元件需要知道總記錄條數,將第一步的總id集合條數返回即可。

從單次查詢來看,一次查詢分成了兩次,效率不高,但是從多次查詢請求來看,快取命中率會非常高,對資料庫壓力極小,資料庫的查詢將主要集中在對id集合的查詢,但是由於索引表的存在,這個查詢效能將極高。

四、總結

以上是我常用的一種資料查詢和快取的方案,由於解釋起來較為繁瑣,所以一直懶得寫,但是實際用下來無論是查詢效率還是開發效率都極高效。希望能對大家有所幫助。

另外,這種方案不適用於資料量極大的情況,因為這種情況下獲取全部id集合的成本極高,但適合絕大部分應用場景。