我想朋友们对缓存已经有一个大致的认识了。从一些朋友的评论中,我了解到有些人也是基于理解,对应用来说可能还是有点力不从心。今天我们就实际案例来分析下缓存的具体应用,就拿博客来说吧。
先分析下博客的网站的特点:页面简单(结构一致)、多用户、多文章、多评论、访问量大等。
页面简单:几乎所有的页面都是头部标题+侧边栏+列表或内容+评论;
多用户:每一个博客都是一个用户,所以可以想想每打开一个页面都会去调用博客表的信息;
多文章:每一个博客都有多篇文章,用户越多文章就更多,上千万篇文章也很正常;
多评论:文章的量已经很大了,评论又怎么会小呢;
访问量大:访问量主要看网站是否受欢迎,我们当然是朝着大访问量的目标设计的。访问量主要集中在文章内容页,因为很多人都是来看文章的,而不是看列表的;
缓存设计的目的就是尽量的减少数据库的压力。因为访问量大的,数据库的连接数会达到顶峰,会造成很多请求会等待;而且数据库的操作属于磁盘操作,在这种连接数满的压力下,整个系统的瓶颈都在磁盘等待(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),自动把请求分配到合适的服务器上。所以,我们的本地缓存的代码就不适合这种部署环境。也就称不上是大的访问量。 虽然如此,今天的代码亦可以让不太了解的朋友知道如何使用缓存。后面的部分我们会说如何解决多个进程 共享缓存的问题。