我想朋友們對緩存已經有一個大致的認識了。從一些朋友的評論中,我瞭解到有些人也是基於理解,對應用來說可能還是有點力不從心。今天我們就實際案例來分析下緩存的具體應用,就拿博客來說吧。
先分析下博客的網站的特點:頁面簡單(結構一致)、多用戶、多文章、多評論、訪問量大等。
頁面簡單:幾乎所有的頁面都是頭部標題+側邊欄+列表或內容+評論;
多用戶:每一個博客都是一個用戶,所以可以想想每打開一個頁面都會去調用博客表的信息;
多文章:每一個博客都有多篇文章,用戶越多文章就更多,上千萬篇文章也很正常;
多評論:文章的量已經很大了,評論又怎麼會小呢;
訪問量大:訪問量主要看網站是否受歡迎,我們當然是朝着大訪問量的目標設計的。訪問量主要集中在文章內容頁,因爲很多人都是來看文章的,而不是看列表的;
緩存設計的目的就是儘量的減少數據庫的壓力。因爲訪問量大的,數據庫的連接數會達到頂峯,會造成很多請求會等待;而且數據庫的操作屬於磁盤操作,在這種連接數滿的壓力下,整個系統的瓶頸都在磁盤等待(IOwait)上。所以,如果沒有緩存,這個系統對於訪問量大的網站來講,自然是不可用,等於廢品。所以,在需要獲取數據的時候,如果這個數據可以被緩存,則否考慮先從緩存裏獲取,如果獲取不到,再去數據庫獲取;然後把獲取的結果放入緩存,再返回結果。
博客緩存:
上面提到,幾乎所有的頁都含有用戶的信息,比如標題、副標題、皮膚等信息,而且一般的訪問規則(URL)裏都包含了此博客的用戶名;所以,博客的信息是必須要緩存的,而且要以用戶名(username)緩存,因爲通過URL只能拿到username,所以用username做key作爲合適。如果緩存中不存在該用戶,則需要通過用戶名查找數據庫,所以數據庫要對username字段建立索引。
另外,頁面簡單一致,說明每個頁都會包含頭部和側邊欄,這些內容是不變化的,所以這些內容最好也要緩存。側邊欄一般包含用戶信息、統計信息(積分、排名等)、分類、TAG、存檔、排行榜等等。這些信息,有的是不及時的,比如排名,分類等,有的卻是需要及時更新的,比如分類的具體文章數量,存檔的具體文章數量。那麼只能具體數據類型,採用具體的策略。比如排名,可以每天更新一次,則按到期時間緩存;而積分和各種統計數字則需要時時更新,所以可以永久緩存,但需要在更新時及時更新這些緩存。比如,用戶發佈一篇文章之後,需要更新積分、分類統計、存檔統計、TAG統計,這些更新操作,自然帶來了開發的複雜度,但他們也有一個共性,就是都屬於側邊欄,所以爲了降低代碼的複雜度,我們把這些緩存全部放在一個緩存裏,只要有更新,就移除整個緩存,然後重新建立新的數據。 雖然這麼做更新的粒度太大,但博客的總訪問量是以讀爲主,這點移除不會有大礙。so...我們來寫用戶的緩存代碼吧
/// <summary>
/// 博客緩存使用演示類
/// </summary>
public class BlogHelper
{
/// <summary>
/// 獲取一個博客信息
/// </summary>
public static Blog GetBlog(string userName)
{
//從緩存獲取博客
var blog = BlogCacheDataProvider.Get(userName);
//如果緩存不存在
if (blog == null)
{
//從數據庫獲取博客信息
blog = DataProvider.GetBlog(userName);
//把信息存入緩存
BlogCacheDataProvider.Set(blog);
}
return blog;
}
/// <summary>
/// 獲取博客的分類
/// </summary>
public static IEnumerable<Category> GetCategories(string userName)
{
//獲取博客
var blog = GetBlog(userName);
//如果博客的分類不存在
if (blog.Categories == null)
{
//從數據庫查詢分類,並賦值給緩存(由於緩存是本地緩存,所以這個賦值會直接修改緩存的信息)
blog.Categories = DataProvider.GetCategories(userName);
}
//返回分類
return blog.Categories;
}
/// <summary>
/// 保存一篇文章
/// </summary>
public static void SaveArticle(Article article)
{
//先保存到數據庫
DataProvider.SaveArticle(article);
//再移除博客的整個緩存
BlogCacheDataProvider.Remove(article.UserName);
}
}
public class BlogCacheDataProvider
{
/// <summary>
/// 博客緩存採用了TimeSpan的過期策略,因爲是整體緩存,而且我們還要手動維護,所以不怕他不過期,而越熱門的用戶,緩存時間越長。冷門緩存會很快消失。默認是1天的緩存時間。
/// </summary>
private static CacheByTimeSpan<string, Blog> _cache;
BlogCacheDataProvider()
{
_cache = new CacheByTimeSpan<string, Blog>();
}
/// <summary>
/// 獲取博客緩存
/// </summary>
public static Blog Get(string userName)
{
return _cache.Get(userName);
}
/// <summary>
/// 更新博客緩存
/// </summary>
public static void Set(Blog blog)
{
_cache.Add(blog.UserName, blog, new TimeSpan(24, 0, 0));
}
/// <summary>
/// 移除博客緩存
/// </summary>
public static void Remove(string userName)
{
_cache.Remove(userName);
}
}
/// <summary>
/// 數據庫操作演示類
/// </summary>
public class DataProvider
{
public static Blog GetBlog(string userName)
{
return new Blog();
}
public static IEnumerable<Category> GetCategories(string userName)
{
return new List<Category>();
}
public static bool SaveArticle(Article article)
{
return true;
}
}
/// <summary>
/// 博客
/// </summary>
public class Blog
{
public string UserName { get; set; }
public IEnumerable<Category> Categories { get; set; }
}
/// <summary>
/// 博客的文章分類
/// </summary>
public class Category { }
/// <summary>
/// 文章
/// </summary>
public class Article
{
public int ArticleId { get; set; }
/// <summary>
/// 文章的文件名(也就是URL裏的英文名)
/// </summary>
public string FileName { get; set; }
/// <summary>
/// 文章內容
/// </summary>
public string Content { get; set; }
public string UserName { get; set; }
}
文章的內容頁是網站訪問量最大的地方,能達到80%以上,所以文章的數據查詢是最大的。而文章的URL一般包含文章的ID號或文章的英文名稱,所以文章的緩存需要2個key,但之前我們介紹的緩存中並沒有兩個key的概念,所以解決辦法就是存2份,畢竟有英文名的文章很少,所以絕大多數來說還都是1份。文章的緩存不僅僅用於用戶的瀏覽,還包括評論的操作也需要查詢文章的數據,所以文章的緩存十分之有必要。看一下文章的緩存代碼吧,因爲重複很多,我只貼出主要部分,其餘的參考博客的代碼即可。
/// <summary>
/// 文章緩存數據提供類
/// </summary>
public class ArticleCacheDataProvider
{
/// <summary>
/// 文章緩存依然採用TimeSpan,根據訪問調整過期時間
/// </summary>
private static CacheByTimeSpan<string,Article> _cache;
private static TimeSpan _cacheTimeSpan;
ArticleCacheDataProvider()
{
_cache = new CacheByTimeSpan<string,Article>();
_cacheTimeSpan = new TimeSpan(24, 0, 0);
}
public static Article Get(string articleIdOrFileName)
{
return _cache.Get(articleIdOrFileName);
}
public static void Set(Article article)
{
_cache.Add(article.ArticleId.ToString(), article, _cacheTimeSpan);
//如果文章有別名,則再緩存一份別名緩存
if (!String.IsNullOrEmpty(article.FileName))
{
_cache.Add(article.FileName, article, _cacheTimeSpan);
}
}
public static void Remove(string articleId, string fileName = null)
{
_cache.Remove(articleId);
_cache.Remove(fileName);
}
}
評論緩存:
評論包含三個重要的關係屬性,一是屬於某篇文章(ArticleId),二是屬於某個博客(BlogId),三是屬於某個發表的人(UserName)。所以對這三個字段都需要建立索引。而在展示的時候,則根據ArticleId來調用。由於評論要求的及時性比較高(不可能說用戶發表了評論之後還要等半天纔可以看到,最多2分鐘的忍受),所以評論最好是在發表或刪除後立即更新緩存。那麼評論的緩存更新也需要手工操作,每當有增加或刪除的時候都要更新掉緩存。
public class Comment
{
public int CommentId { get; set; }
public int ArticleId { get; set; }
public int BlogId { get; set; }
public string UserName { get; set; }
}
public class CommentCacheDataProvider
{
/// <summary>
/// 評論的緩存一般是列表的緩存
/// </summary>
private static CacheByTimeSpan<int, List<Comment>> _cache;
CommentCacheDataProvider()
{
_cache = new CacheByTimeSpan<int, List<Comment>>();
}
public static IEnumerable<Comment> Get(int articleId)
{
return _cache.Get(articleId);
}
/// <summary>
/// 增加一個評論
/// </summary>
/// <param name="comment"></param>
public static void AddComment(Comment comment)
{
var list = _cache.Get(comment.ArticleId);
list.Add(comment);
}
/// <summary>
/// 刪除一個評論
/// </summary>
/// <param name="comment"></param>
public static void DeleteComment(Comment comment)
{
var list = _cache.Get(comment.ArticleId);
foreach (var c in list)
{
if (c.CommentId == comment.CommentId)
{
list.Remove(c);
break;
}
}
}
/// <summary>
/// 移除緩存
/// </summary>
/// <param name="articleId"></param>
public static void Remove(int articleId)
{
_cache.Remove(articleId);
}
}
從代碼中,信心的朋友可以看到有Add和Delete對應的操作緩存的實現,而不需要移除整個緩存。事實上我們可以對任意緩存做操作,而不需要讓緩存過期。但這有一個明顯的問題,上面也提到過就是代碼的複雜度。不過,凡事都有正反兩面,有得必有失。這需要根據具體的情況採用具體的應對策略。
訪問量大:
我們上面的例子都是基於本地緩存的實現。什麼是本地緩存呢?我們知道ASP.NET站點的程序(application)是駐留在應用程序池的,也就是w3wp.exe進程。每個站點對應一個進程。然而,真正的大訪問量網站,一個進程是搞不定的,一般需要多臺機器上部署,這些機器的前端有一個負載均衡(load balance),自動把請求分配到合適的服務器上。所以,我們的本地緩存的代碼就不適合這種部署環境。也就稱不上是大的訪問量。 雖然如此,今天的代碼亦可以讓不太瞭解的朋友知道如何使用緩存。後面的部分我們會說如何解決多個進程共享緩存的問題。