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