我也簡單談下《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集合的成本極高,但適合絕大部分應用場景。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM