摘要:一般大家做的緩存都是實時更新,並且用LRU算法實現緩存過期策略,但當緩存越來越大的時候,對緩存做的線程同步會導致應用的響應便慢。如何更有效的使用緩存,如何提高緩存命中率,如何減少對緩存加鎖操作,如何提高緩存的性能,我們來討論一下。
1、找出活躍數據,我們用一種分離的方式來找出活躍數據,單獨寫一個提取活躍數據的後台程序從數據庫裡統計出最近一小時查閱次數最多的前1w篇文章的ID,這些文章肯定是用戶最常訪問的文章,把這些文章的數據取出來用FTP上傳到緩存服務器上,並發消息給緩存服務器通知它有新的緩存數據可用了,因為提取的都是活躍數據,所以這樣的數據做緩存命中率會高一些。
2、緩存服務器收到提取活躍數據程序的通知後,從本機磁盤上讀取緩存信息加載到內存裡,並替換到上次使用的緩存,這樣緩存就不是實時更新的,而是相對只讀的,每1小時才更新一次,所以使用緩存不需要加鎖,只需使用緩存的時候把緩存對象用一個臨時變量引用一下,防止在新舊緩存交替的時候被變為null。
3、用一個單獨的DB來作為數據版本數據庫,裡面保存著每篇文章的版本信息,版本信息是int類型的,無論誰修改了某篇文章,都要在版本數據庫裡讓相應的文章版本號加1,寫一個版本管理服務提供查詢某個文章版本和更新某個文章版本的功能。因為版本數據庫的字段大多都是int型,表應該很窄,性能不會很差。
4、用戶請求一篇文章的時候,先看緩存服務器有沒有,如果沒有,直接從數據庫裡取出來;如果有,取出緩存數據版本號,並從版本服務器上獲取該文章真實版本號,如果一直,就使用緩存數據,如不一直,從數據庫裡取出文章數據,並更新緩存。這裡雖然用戶的每個請求都要訪問版本數據庫,但因為版本數據庫結構簡單,容易優化,所以出現性能瓶頸的的可能性比較小,而如果緩存命中率足夠高的話能減少大量對文章數據庫的請求。
using System;
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、因為判斷數據版本號和使用緩存數據不是一個原子操作,在這中間數據版本號可能會更新,所以在高並發的情況下,可能給用戶顯示了比較舊的數據,只有用戶再次刷新才會發現文章版本號變了而使用最新數據,這裡就的犧牲用戶體驗換取性能了。