在前面的文章中也提及到为了提高系统的性能,SpaceBuilder在内部做了大量的工作,而数据缓存就是其中非常高效的处理方式.
我们知道SpaceBuilder采用了多层架构的处理模式,数据通过业务实体的形式在系统框架的各个层内传递.(如图1)
图表 1SpaceBuilder中数据(业务实体)在各个框架层间的传递
业务实体是在各层间进行数据传递的形式,但其最初都来源于各种序列化的第三方的存储介质(比如此处的SQLServer数据库)中.我们知道这各种 第三方存储介质由于要进行各种I/O操作,其访问速度和响应性能肯定会受到一定的约束.为了提高访问速度获取更好的响应性能,那么我们是否可以考虑把业务 实体从各种序列化存储介质剥离出来,将其放入性能更快的不需要频繁进行I/O操作的介质(比如内存)中呢? SpaceBuilder就是这么做的.
当然采用内存保存业务信息,速度效率是能得到保证了,但是其有一个致命的缺点—数据不能持久化,也就是说当一断电的时候,内存中所有的数据都被清空 了.这样不能保存用户的业务数据肯定是不可以的.而我们的系统又要求有较高的性能?那有如何办呢? SpaceBuilder就在使用内存和第三方介质保存业务数据间,进行了一定的取舍和平衡.什么意思呢? 所有的数据都真实的保存的第三方介质中,而在内存中对其做镜像;也就是说数据在系统中存在两份.这个时候,有一件事情就变的尤其重要,那就是两份数据同步 的问题. 当然我们可以通过一定的策略对其进行保证(下面会有专门的文字谈到这个问题).
系统中会有两份数据,那么这两份数据都在什么情况下使用呢?首先我们使用网络通常会有这么一个体验(或者说习惯): 因为我们上网的目的很大程度上是在获取信息,那么我们去浏览信息的的时间可能会占到我们总上网时间的95%,甚至更多, 而我们为了展示自己而发布信息 的时候就会很少,几乎连5%都占不到. 那么我们将这两个行为进行抽象,浏览信息对应我们开发来说肯能就是 Get(),或者是GetList()方法;而发布信息就是Add(),Update(),Delete()方法. 这样我们就可以吧改变业务数据的方式(Add/Update/Delete)其直接操作第三方存储介质,而不改变业务数据的方法(Get /GetList)直接读取内存. 这无疑即保存了用户的数据,也提高了访问速度(因为95%或者更多时候大家都是在Get数据,而Get对应的操作对象是响应速度更多的内存).
SpaceBuilder中的缓存都统一封装在了WebCache类中.WebCache类功能非常强大,同时使用也非常简单.
l Insert()—将某一个(一批)业务数据,保存入缓存体系中;
l Remove()—将某一个(一批)业务数据,从缓存体系中移除;
l Get()—从缓存体系中获取某一个(一批)业务数据
那么我们研究WebCache就先从获取业务数据(Get方法)看起.Components中所有的Get方法几乎都采用了相似的处理逻辑,我们可以随便找一个(此处我们以Files/Components/FileSections.cs 142行为例)
public static FileSection GetSection(int sectionID, bool userCache)
{
string cacheKey = string.Format(“FileSection-SectionID:{0}”, sectionID);
FileSection section = null;
if (userCache)
section = WebCache.Get(cacheKey) as FileSection;
if (section == null)
{
section = FileDataProvider.Instance().GetSection(sectionID);
if (userCache)
WebCache.Insert(cacheKey, section, WebCache.MinuteFactor * 2);
}
return section;
}
首先其 创建了一个缓存的键string cacheKey = string.Format(“FileSection-SectionID:{0}”, sectionID); 接着先去系统缓存中根据这个键去获取 相应的值WebCache.Get(cacheKey) as FileSection.如果这个值没有取到if (section == null),那么就再通过第三方介质获取section = FileDataProvider.Instance().GetSection(sectionID)(这个方法实际上使用配置中设置的数据提供者(可 能是SqlServer)获取业务数据,这个知识点在数据库访问中提及,可以参考这个《浅谈SpaceBuilder系统中的数据访问体系》).最后在把 这个获取到业务数据返回给调用方法前先放入到缓存中一份,供此客户或其他客户下次调用(也就是说当系统启动后,第一次请求某个业务数据的时候,缓存中是肯 定不会存在的,那么其将从第三方介质中获取到,第二次(及其后续)访问的时候,就能够使用第一次访问是生成在内存中的缓存数据了.) 这也是一个每请求俱缓存的思想.
那么会改变业务数据的操作,在SpaceBuilder里面是怎样进行的操作呢?我们看这个类的Update方法
public static int Update(FileSection section)
{
// Make sure it is clean
section.SectionName = HtmlScrubber.Clean(section.SectionName, false, true);
section.Description = HtmlScrubber.Clean(section.Description, false, true);
// Censor the stuff, if enabled
if (SiteSettingsManager.GetSiteSettings().EnableCensorship)
{
section.SectionName = Censors.CensorPost(section.SectionName);
section.Description = Censors.CensorPost(section.Description);
}
FileDataProvider.Instance().CreateUpdateSection(section, DataProviderAction.Update);
//清除缓存
WebCache.Remove(string.Format(“FileSection-SectionID:{0}”, section.SectionID));
WebCache.Remove(string.Format(“FileSection-ApplicationKey:{0}”, section.ApplicationKey));
WebCache.RemoveByPattern(“FileSections::”);
// Return the ID
return section.SectionID;
}
我们看到84行,首先其通过数据提供者操作更新第三方介质中的业务数据 FileDataProvider.Instance().CreateUpdateSection(section, DataProviderAction.Update);然后87-89行根据一定的键,从缓存中移除掉相应的业务数据(因为缓存中的业务数据跟第三方介 质中的数据的状态已经不一致了,因此将其移除;等下次用户请求的时候再重新从第三方介质中读取并且将新的数据写入缓存中). 也就是说每当有业务数据的状态改变的时候,都要同时更新缓存中的数据,以保证两份的数据的状态一致.
如果大家有兴趣,还可以查看一下WebCache类的内部实现方式.
我们走正常的思维,通过构造函数解读这个类
static WebCache()
{
HttpContext context = HttpContext.Current;
if (context != null)
_cache = context.Cache;
else
_cache = HttpRuntime.Cache;
}
看完这个构造函数,我们想大家应就会明白 SpaceBuilder中缓存的数据是通过什么方式保存在内存中的了.不错就是dotNet的系统缓存(我们可以通过HTTPContext上下文访问 到这个对象), SpaceBuilder只是在这个对象上进行了一次包装,包装后的新类更加面向我们的开发人员—接口更加友好,调用更加方便.那么我就看一下这个类的几 个常用方法:
l Get()最常用也最简单
由于系统的Cache对象本身就是一个可枚举类型,因此我们可以直接通过索引的方式访问
public static object Get(string key)
{
return _cache[key];
}
l Insert()向缓存中插入数据
///
/// 加入缓存项
///
/// 缓存项标识
/// 缓存项
/// 缓存依赖
/// 缓存秒基数(最终秒数的计算为Factor * secondsBase)
/// 缓存优先级
public static void Insert(string key, object obj, CacheDependency dep, int secondsBase, CacheItemPriority priority)
{
if (obj != null)
_cache.Insert(key, obj, dep, DateTime.Now.AddSeconds(Factor * secondsBase), TimeSpan.Zero, priority, null);
}
Insert()方法也很简单,直接调用系统Cache的Insert()方法,将一个键(缓存的名称标识),一个值(缓存的具体对象),还用其他的辅助参数(比如缓存的时间,缓存的级别,缓存的依赖对象等)
l Remove()从缓存中删除数据
public static void Remove(string key)
{
_cache.Remove(key);
}
调用系统Cache的Remove()方法
l RemoveByPattern()通过正则表达式删除相匹配的数据
public static void RemoveByPattern(string pattern)
{
IDictionaryEnumerator cacheEnum = _cache.GetEnumerator();
Regex regex = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
while (cacheEnum.MoveNext())
{
if (regex.IsMatch(cacheEnum.Key.ToString()))
_cache.Remove(cacheEnum.Key.ToString());
}
}
这个方法其实也简单就是 先遍历系统Cache内所有的数据,然后通过正则表达式验证是否匹配,决定是否删除.