带你走进缓存世界(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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章