zoukankan      html  css  js  c++  java
  • 带你走进缓存世界(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),自动把请求分配到合适的服务器上。所以,我们的本地缓存的代码就不适合这种部署环境。也就称不上是大的访问量。 虽然如此,今天的代码亦可以让不太了解的朋友知道如何使用缓存。后面的部分我们会说如何解决多个进程共享缓存的问题。

  • 相关阅读:
    python3.6中 字典类型和字符串类型互相转换的方法
    "sorted()"中的"Key Functions"
    tuple unpacking
    理解"__repr__"
    Python中的"Special Method"
    abstractmethod
    JavaScript括号中什么什么不加引号什么时候加引号?
    加载网页时速度慢的一些知识点
    Login登录页面的制作流程(摘要)
    JavaScript总结1
  • 原文地址:https://www.cnblogs.com/mad/p/2131156.html
Copyright © 2011-2022 走看看