蛙蛙推荐:设计一个高效的缓存管理服务
蛙蛙推荐:设计一个高效的缓存管理服务
摘要:一般大家做的缓存都是实时更新,并且用LRU算法实现缓存过期策略,但当缓存越来越大的时候,对缓存做的线程同步会导致应用的响应便慢。如何更有效的使用缓存,如何提高缓存命中率,如何减少对缓存加锁操作,如何提高缓存的性能,我们来讨论一下。
1、找出活跃数据,我们用一种分离的方式来找出活跃数据,单独写一个提取活跃数据的后台程序从数据库里统计出最近一小时查阅次数最多的前1w篇文章的ID,这些文章肯定是用户最常访问的文章,把这些文章的数据取出来用FTP上传到缓存服务器上,并发消息给缓存服务器通知它有新的缓存数据可用了,因为提取的都是活跃数据,所以这样的数据做缓存命中率会高一些。
2、缓存服务器收到提取活跃数据程序的通知后,从本机磁盘上读取缓存信息加载到内存里,并替换到上次使用的缓存,这样缓存就不是实时更新的,而是相对只读的,每1小时才更新一次,所以使用缓存不需要加锁,只需使用缓存的时候把缓存对象用一个临时变量引用一下,防止在新旧缓存交替的时候被变为null。
3、用一个单独的DB来作为数据版本数据库,里面保存着每篇文章的版本信息,版本信息是int类型的,无论谁修改了某篇文章,都要在版本数据库里让相应的文章版本号加1,写一个版本管理服务提供查询某个文章版本和更新某个文章版本的功能。因为版本数据库的字段大多都是int型,表应该很窄,性能不会很差。
4、用户请求一篇文章的时候,先看缓存服务器有没有,如果没有,直接从数据库里取出来;如果有,取出缓存数据版本号,并从版本服务器上获取该文章真实版本号,如果一直,就使用缓存数据,如不一直,从数据库里取出文章数据,并更新缓存。这里虽然用户的每个请求都要访问版本数据库,但因为版本数据库结构简单,容易优化,所以出现性能瓶颈的的可能性比较小,而如果缓存命中率足够高的话能减少大量对文章数据库的请求。
using System.Collections.Generic;
using System.Threading;
using System.Diagnostics;
namespace DataCache {
//可缓存的实体类
public class Canbecached{public int Version;}
public class Article : Canbecached { }
public class CacheItem : Canbecached
{
public Article Article;
}
//每个文章都在版本数据库里保存版本号,该接口提供查询版本方法
//如果某个文章修改了,要调用这个接口的UpdateXXVersion方法
//来更新数据版本,以便让缓存服务器可以得到真实数据的最新的版本
public interface VersionManager
{
int GetArticleVersion(int articleId);
void UpdateArticleVserion(int articleId);
}
//该类用于管理缓存,以及提供缓存使用接口
//缓存和使用缓存的服务不一定是一个进程,甚至不是一台机器,通过socket
//或者Remoting来使用及更新缓存你
internal static class ArticleCacheManager
{
private static volatile bool _cacheAvailable = false;
static Dictionary<int, CacheItem> _cache = new Dictionary<int, CacheItem>();
public static bool CacheAvailable {
get { return _cacheAvailable; }
}
public static void InitCache()
{
_cache = new Dictionary<int, CacheItem>();
_cacheAvailable = true;
}
public static void LoadCache()
{
Dictionary<int, CacheItem> cache = readCacheFromDisk();
_cache = cache; //该操作是线程安全的,不需要lock(_cahce),因为使用_cache的时候都先放到临时变量里再使用的。
}
private static Dictionary<int, CacheItem> readCacheFromDisk() {
throw new NotImplementedException();
}
private static void checkCacheAndArticleId(int articleId) {
if (!_cacheAvailable) throw new InvalidOperationException("Cache not Init.");
if (articleId < 1) throw new ArgumentException("articleId");
}
internal static VersionManager GetVersionManager()
{
throw new NotImplementedException();
}
internal static Article GetArticle(int articleId) {
checkCacheAndArticleId(articleId);
Dictionary<int, CacheItem> cache = _cache;
CacheItem item;
if (cache.TryGetValue(articleId, out item))
return item.Article;
else
return null;
}
internal static void UpdateArticle(int articleId, Article article) {
checkCacheAndArticleId(articleId);
Dictionary<int, CacheItem> cache = _cache;
CacheItem item;
if (cache.TryGetValue(articleId, out item))
item.Article = article; //这个赋值操作是线程安全的,不需要lock这个Item。
}
}
//从数据库里读取文章信息
internal static class DBAdapter
{
internal static Article GetArticle(int articleId, bool IsUpdateCache) {
Article article = new Article();
if(IsUpdateCache)ArticleCacheManager.UpdateArticle(articleId, article);
throw new NotImplementedException();
}
}
//用来保存一个文章
public class ArticleItem
{
public int ArticleId;
public ArticleItem(int articleId)
{
ArticleId = articleId;
}
public Article Article;
public void Load()
{
//1、缓存正在切换到时候直接从DB取数据
if(!ArticleCacheManager.CacheAvailable)
{
Article = DBAdapter.GetArticle(ArticleId,false);
return;
}
VersionManager versionManager = ArticleCacheManager.GetVersionManager();
//2、比较缓存版本和真实数据版本确定是否使用缓存信息
DataCache.Article article = ArticleCacheManager.GetArticle(ArticleId);
if(article != null && article.Version == versionManager.GetArticleVersion(ArticleId))
Article = ArticleCacheManager.GetArticle(ArticleId); //尽管这里判断了版本,但也有可能取到旧数据,因为当你获取数据版本并决定使用缓存数据的时候,可能恰好用户修改了文章数据,这种情况只要等用户下次刷新一下页面了,用户体验并不是太差。
else
Article = DBAdapter.GetArticle(ArticleId, article != null);//如果article不是null,说明只是缓存数据版本太旧,这时候要把从数据库取出的数据更新到缓存里
}
}
class Program {
static Dictionary<int, ArticleItem> _articles = new Dictionary<int, ArticleItem>();
static void Main(string[] args)
{
//初始化缓存
ArticleCacheManager.InitCache();
//检查是否有新的缓存可用
new Thread(checkCacheProc).Start();
//用户请求一篇文章
ArticleItem article1 = new ArticleItem(1);
article1.Load();
}
public static void checkCacheProc(Object state)
{
Thread.CurrentThread.Name = "Check whether there is a new cache Thread";
while (true)
{
try {
if (newCacheAvailable())
ArticleCacheManager.LoadCache();
Thread.Sleep(TimeSpan.FromMinutes(60));
}
catch (Exception ex) {
Trace.TraceError("check cache occur an error:"+ ex.Message);
}
}
}
private static bool newCacheAvailable() {
throw new NotImplementedException();
}
}
}
不足
1、更新文章的时候需要更新版本数据库,还要用一个事务来保证一致性,需要更改现有代码,并且降低写性能。不过我觉得这比用户更新数据的时候同时通知缓存服务器还是要简单一些,那样更复杂,还是尽量保证设计简单吧,如果版本数据库撑不住了再试试这种方案。
2、因为判断数据版本号和使用缓存数据不是一个原子操作,在这中间数据版本号可能会更新,所以在高并发的情况下,可能给用户显示了比较旧的数据,只有用户再次刷新才会发现文章版本号变了而使用最新数据,这里就的牺牲用户体验换取性能了。