帶你走進緩存世界(5):一顯身手

        我想朋友們對緩存已經有一個大致的認識了。從一些朋友的評論中,我瞭解到有些人也是基於理解,對應用來說可能還是有點力不從心。今天我們就實際案例來分析下緩存的具體應用,就拿博客來說吧。
 
         先分析下博客的網站的特點:頁面簡單(結構一致)、多用戶、多文章、多評論、訪問量大等。
                 頁面簡單:幾乎所有的頁面都是頭部標題+側邊欄+列表或內容+評論;
                 多用戶:每一個博客都是一個用戶,所以可以想想每打開一個頁面都會去調用博客表的信息;
                 多文章:每一個博客都有多篇文章,用戶越多文章就更多,上千萬篇文章也很正常;
                 多評論:文章的量已經很大了,評論又怎麼會小呢;
                 訪問量大:訪問量主要看網站是否受歡迎,我們當然是朝着大訪問量的目標設計的。訪問量主要集中在文章內容頁,因爲很多人都是來看文章的,而不是看列表的;


         緩存設計的目的就是儘量的減少數據庫的壓力。因爲訪問量大的,數據庫的連接數會達到頂峯,會造成很多請求會等待;而且數據庫的操作屬於磁盤操作,在這種連接數滿的壓力下,整個系統的瓶頸都在磁盤等待(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),自動把請求分配到合適的服務器上。所以,我們的本地緩存的代碼就不適合這種部署環境。也就稱不上是大的訪問量。 雖然如此,今天的代碼亦可以讓不太瞭解的朋友知道如何使用緩存。後面的部分我們會說如何解決多個進程共享緩存的問題。


發佈了42 篇原創文章 · 獲贊 191 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章