Orchard详解--第六篇 CacheManager 2
接上一篇,关于ICacheContextAccessor先看一下默认实现,用于保存一个获取上下文,且这个上下文是线程静态的:
    public class DefaultCacheContextAccessor : ICacheContextAccessor {
        [ThreadStatic]
        private static IAcquireContext _threadInstance;
        public static IAcquireContext ThreadInstance {
            get { return _threadInstance; }
            set { _threadInstance = value; }
        }
        public IAcquireContext Current {
            get { return ThreadInstance; }
            set { ThreadInstance = value; }
        }
    }
在上一篇也提到获取上下文主要用于保存一个Key和对应的Token,用于验证对应Key的缓存是否过期。
讲到这先看一个例子:
private void CacheTest()
{
var time1 = _cacheManager.Get("Time1", ctx => {
ctx.Monitor(_clock.When(TimeSpan.FromHours()));
return GetStringFromCache();
});
Thread.Sleep();
var time2 = _cacheManager.Get("Time1", ctx => {
ctx.Monitor(_clock.When(TimeSpan.FromHours()));
return GetStringFromCache();
});
} private string GetStringFromCache()
{
return _cacheManager.Get("Time", ctx => {
ctx.Monitor(_clock.When(TimeSpan.FromSeconds()));
return DateTime.Now.ToString();
});
}
Key为Time1的缓存包含了另一个Key为Time的缓存,并且Time1的有效时间为1小时而Time的有效时间只有5秒。那么问题来了Time1必须等待1小时再去更新吗?但是Time的值已经更新N次了?先看一下调试结果:


发现两次结果不一致(因为断点暂停所以时间超过5秒)。
看一下缓存中的实际内容:
Time:

Time1:

有没有发现Time1有两个Token?


并且第二个的IsCurrent属性已经为false了。Why?
让我们再回到Cache的代码:
         private CacheEntry AddEntry(TKey k, Func<AcquireContext<TKey>, TResult> acquire) {
             var entry = CreateEntry(k, acquire);
             PropagateTokens(entry);
             return entry;
         }
         private void PropagateTokens(CacheEntry entry) {
             // Bubble up volatile tokens to parent context
             if (_cacheContextAccessor.Current != null) {
                 foreach (var token in entry.Tokens)
                     _cacheContextAccessor.Current.Monitor(token);
             }
         }
         private CacheEntry CreateEntry(TKey k, Func<AcquireContext<TKey>, TResult> acquire) {
             var entry = new CacheEntry();
             var context = new AcquireContext<TKey>(k, entry.AddToken);
             IAcquireContext parentContext = null;
             try {
                 // Push context
                 parentContext = _cacheContextAccessor.Current;
                 _cacheContextAccessor.Current = context;
                 entry.Result = acquire(context);
             }
             finally {
                 // Pop context
                 _cacheContextAccessor.Current = parentContext;
             }
             entry.CompactTokens();
             return entry;
         }
- 当获取Time1时,因为缓存中不存在Time1,所以进入CreateEntry方法。
 - 因为之前无任何操作,且DefaultCacheContextAccessor也未对_threadInstance初始化,所以_cacheContextAccessor.Current为null。
 - 将新建的AcquireContext作为_cacheContextAccessor.Current,并调用acquire方法。
 - Time1的acquire方法中又需要去缓存中取Time,因为不存在又进入CreateEntry方法,但这次不同的是_cacheContextAccessor.Current不为null。
 - Time通过acquire方法创建了缓存值以及5秒过期的Token,并进入到PropagateTokens方法。
 - PropagateTokens将当前的Token添加到_cacheContextAccessor.Current中,而当前的_cacheContextAccessor.Current实际上是Time1的获取上下文。所以Time1将拥有2个Token。
 
换句话说以上过程保证了当存在缓存嵌套使用时,缓存的上一层一定包含下一层的所有Token,如果下层的缓存失效了,那么上层的一定失效。
在上一篇中还提到了DefaultParallelCacheContext,它又有什么作用呢?
先看一个实际使用的例子:
         public IEnumerable<ExtensionDescriptor> AvailableExtensions() {
             return _cacheManager.Get("AvailableExtensions", true, ctx =>
                 _parallelCacheContext
                     .RunInParallel(_folders, folder => folder.AvailableExtensions().ToList())
                     .SelectMany(descriptors => descriptors)
                     .ToReadOnlyCollection());
         }
这个例子根据代码表面意思来看是以并行的方式将每个folder下的拓展信息获取出来。
看一下RunInParallel的实现细节:
         public IEnumerable<TResult> RunInParallel<T, TResult>(IEnumerable<T> source, Func<T, TResult> selector) {
             if (Disabled) {
                 return source.Select(selector);
             }
             else {
                 // Create tasks that capture the current thread context
                 var tasks = source.Select(item => this.CreateContextAwareTask(() => selector(item))).ToList();
                 // Run tasks in parallel and combine results immediately
                 var result = tasks
                     .AsParallel() // prepare for parallel execution
                     .AsOrdered() // preserve initial enumeration order
                     .Select(task => task.Execute()) // prepare tasks to run in parallel
                     .ToArray(); // force evaluation
                 // Forward tokens collected by tasks to the current context
                 foreach (var task in tasks) {
                     task.Finish();
                 }
                 return result;
             }
         }
         /// <summary>
         /// Create a task that wraps some piece of code that implictly depends on the cache context.
         /// The return task can be used in any execution thread (e.g. System.Threading.Tasks).
         /// </summary>
         public ITask<T> CreateContextAwareTask<T>(Func<T> function) {
             return new TaskWithAcquireContext<T>(_cacheContextAccessor, function);
         }
         public class TaskWithAcquireContext<T> : ITask<T> {
             private readonly ICacheContextAccessor _cacheContextAccessor;
             private readonly Func<T> _function;
             private IList<IVolatileToken> _tokens;
             public TaskWithAcquireContext(ICacheContextAccessor cacheContextAccessor, Func<T> function) {
                 _cacheContextAccessor = cacheContextAccessor;
                 _function = function;
             }
         }
这段代码主要做了三件事情:
- 一开始的时候它为每一个元素(每一个Folder)附加上了一个AcquireContext,即TaskWithAcquireContext既包含用来获取元素的folder => folder.AvailableExtensions().ToList()表达式还包含了一个ICacheContextAccessor,通过上面的分析可知,ICacheContextAccessor用于缓存中存在包含其它缓存的情况。
 - 并行处理每一个元素(每一个Folder)调用Execute方法。
 - 完成后针对每一个Task调用Finish方法。
 
接下来看一下Execute和Finish方法的实现:
/// <summary>
/// Execute task and collect eventual volatile tokens
/// </summary>
public T Execute() {
IAcquireContext parentContext = _cacheContextAccessor.Current;
try {
// Push context
if (parentContext == null) {
_cacheContextAccessor.Current = new SimpleAcquireContext(AddToken);
} // Execute lambda
return _function();
}
finally {
// Pop context
if (parentContext == null) {
_cacheContextAccessor.Current = parentContext;
}
}
} /// <summary>
/// Return tokens collected during task execution
/// </summary>
public IEnumerable<IVolatileToken> Tokens {
get {
if (_tokens == null)
return Enumerable.Empty<IVolatileToken>();
return _tokens;
}
} public void Dispose() {
Finish();
} /// <summary>
/// Forward collected tokens to current cache context
/// </summary>
public void Finish() {
var tokens = _tokens;
_tokens = null;
if (_cacheContextAccessor.Current != null && tokens != null) {
foreach (var token in tokens) {
_cacheContextAccessor.Current.Monitor(token);
}
}
} private void AddToken(IVolatileToken token) {
if (_tokens == null)
_tokens = new List<IVolatileToken>();
_tokens.Add(token);
}
}
是否与Cache中的CreateEntry方法以及PropagateTokens方法类似?只不过SimpleAcquireContext是没有Key这个属性的,只有一个用于添加AddToken的_monitor委托。
最后分析一下上面并行处理缓存的过程:
- CacheManager通过Key"AvailableExtensions"去查找缓存,当第一次查找时缓存中不存在"AvailableExtensions"这个Key,那么调用Cache的CreateEntry方法。
 - 这时就会创建一个AcquireContext(包含当前Key和一个AddToken的Mointor),然后将带着这个Context去执行Acquire方法,而现在的Acquire方法就是包含并行处理的那个代理。
 - 其实也就是执行_parallelCacheContext.RunInParallel这个方法了,执行该方法的时候_cacheContextAccessor.Current已经是Key为AvailableExtensions的AcquireContext了(可以参考上面非并行过程),在通过多个线程完成所有Task之后,每一个Task中包含了改Task所执行的所有的Token。最终通过Finish的方法添加到_cacheContextAccessor.Current中,也就是AvailableExtensions的CacheEntry中。
 
结果AvailableExtensions这个缓存包含一个有16个元素的List,并且存在27个Token,如果其中某一个失效,那么都会刷新缓存:
  
最后的最后来说明一下为什么DefaultCacheContextAccessor的Current属性(或者_threadInstance字段或者ThreadInstance属性)是线程静态的。
在代码中Current属性涉及到的地方都可以看到很多Push Context和Pop Context的注释,通过分析也知道它是为了处理被包含缓存Token而设计的,且每次使用完毕该属性都会被设为null。即每一次都是新的。那么在单线程或者说串行处理的环境下永远没有问题。
但是在并行环境下,如果Current是全静态的,那么该属性就有可能被污染。当我尝试将其改为非静态类型,那么整个程序将无法运行(但抛异常时CacheHolder有部分值,证明仍旧能够添加缓存),该问题待研究。
小结:
经过两篇的CacheManager的分析,主要研究了CacheManager的使用方法和原理。本系列主要目的是分析从Orchard这个框架我们能学习到什么。而CacheManager这一块在我看来设计的非常巧妙(至少自己很难去设计出这样的代码)。所以除了能够了解Orchard缓存运行机制外,更重要的能够感受代码以期望自己能够得到提升...
补充:
之前一直忘了列出Orchard中所有的缓存失效Token,这里补充一下:
异步Token:AsyncVolativeToken。
信号Token:Signals中的内部
命令行相关Token:CommandHostVirtualPathMonitor,内部类型包含和文件、目录相关的Token。
AppDataFolder Token:AppDataFolder
失效Token:InvalidationToken位于DefaultDependenciesFolder和DefaultExtensionDependenciesManager的私有Token。
基于虚拟路径的Token:位于 DefaultVirtualPathMonitor
基于时间的Clock Token:
以上内容是通过搜索IVolatileToken整理出来的,部分Token暂时不知道有什么作用,但是也可以大致猜测。更多的会在后续章节中涉及。
参考:
http://www.cnblogs.com/n-pei/archive/2011/05/01/2033911.html
http://www.bubuko.com/infodetail-186108.html
http://docs.orchardproject.net/en/latest/Documentation/Caching/
以及Orchard源码。
Orchard详解--第六篇 CacheManager 2的更多相关文章
- Orchard详解--第五篇 CacheManager
		
上一篇文章介绍了Orchard中的缓存,本篇主要针对CacheManager进行分析,CacheManager在Orchard中用于存储应用程序的配置信息以及框架内部的一些功能支持,包括整个拓展及拓展 ...
 - Orchard详解--第三篇 依赖注入之基础设施
		
Orchard提供了依赖注入机制,并且框架的实现也离不开依赖注入如模块管理.日志.事件等.在前一篇中提到在Global.asax中定义的HostInitialization创建了Autofac的IoC ...
 - Orchard详解--第八篇 拓展模块及引用的预处理
		
从上一篇可以看出Orchard在处理拓展模块时主要有两个组件,一个是Folder另一个是Loader,前者用于搜索后者用于加载. 其中Folder一共有三个:Module Folder.Core Fo ...
 - Orchard详解--第七篇 拓展模块(译)
		
Orchard作为一个组件化的CMS,它能够在运行时加载任意模块. Orchard和其它ASP.NET MVC应用一样,支持通过Visual Studio来加载已经编译为程序集的模块,且它还提供了自定 ...
 - Orchard详解--第四篇 缓存介绍
		
Orchard提供了多级缓存支持,它们分别是: 1. 应用程序配置级缓存ICacheManager: 它用来存储应用程序的配置信息并且可以提供一组可扩展的参数来处理缓存过期问题,在Orchard中默认 ...
 - Java中JNI的使用详解第六篇:C/C++中的引用类型和Id的缓存
		
首先来看一下C/C++中的引用 从Java虚拟机创建的对象传到本地C/C++代码时会产生引用,根据Java的垃圾回收机制,只要有引用存在就不会触发该引用指向的Java对象的垃圾回收 第一.局部引用: ...
 - 开源项目MultiChoiceAdapter详解(六)——GridView和MultiChoiceBaseAdapter配合使用
		
这篇其实没啥重要的,主要就算是个总结吧. 一.布局文件 这里实现的是类似于上图的多图选择的效果.关键在于item布局文件的写法.这也就是这个框架奇葩的一点,莫名其妙的要在一个自定义控件里面再放一个自定 ...
 - IIS负载均衡-Application Request Route详解第四篇:使用ARR实现三层部署架构(转载)
		
IIS负载均衡-Application Request Route详解第四篇:使用ARR实现三层部署架构 系列文章链接: IIS负载均衡-Application Request Route详解第一篇: ...
 - [转]ANDROID L——Material Design详解(动画篇)
		
转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持! 转自:http://blog.csdn.net/a396901990/article/de ...
 
随机推荐
- [java]__如何用你的编程语言表达至尊宝"爱你一万年"的浪漫情怀.
			
前言 我在很多地方,或多或少都了解到人们对程序员的看法,大多是智商高情商低,不懂的浪漫之类的,并且看到了一个十分有趣的视频,用程序来表达你对女朋友的爱,于是,便来了兴趣,我想最浪漫的承诺,应该就是大话 ...
 - leetcode — set-matrix-zeroes
			
import java.util.Arrays; /** * Source : https://oj.leetcode.com/problems/set-matrix-zeroes/ * * * Gi ...
 - 手把手使用Docker搭建SpringBoot微服务镜像
			
一.环境准备 1.安装好Docker环境的Linux机器(安装教程) 2.准备好SpringBoot项目打包好的可运行jar包 二.编写Dockerfile 1.首先将SpringBoot打包好的ja ...
 - Java线程实现与安全
			
目录 1. 线程的实现 线程的三种实现方式 Java线程的实现与调度 2. 线程安全 Java的五种共享数据 保证线程安全的三种方式 前言 本篇博文主要是是在Java内存模型的基础上介绍Java线程更 ...
 - Markdown——入门使用
			
一 Markdown是什么 markdown是一种纯文本格式的标记语言.通过简单的标记语法,它可以使普通文本具有一定的格式.markdown的语法十分简单,常用的也不过十来个,是一种轻量级的标记语言, ...
 - Spring Day 2
			
**Spring框架的IOC之注解方式的快速入门** 步骤一:导入注解开发所有需要的jar包 步骤二:创建对应的包结构,编写Java的类:接口到实现类 步骤三:在src的目录下,创建applicati ...
 - mssqlserver on linux - Linux下尝鲜MSSQL-SERVER【微软大法棒棒哒】
			
微软的开源精神真是无敌了,接下来体验下Linux安装与使用MSSQL-SERVER! 安装说明 目前支持的平台: Red Hat Enterprise Linux 7.2 Get RHEL 7.2 U ...
 - 12.QT4.7.4-解决WIN平台和Linux平台中文乱码,QLineEdit右击菜单中文显示
			
1.解决Win平台中文显示 1.1首先解决win平台上中文显示乱码问题 1)首先查看qt creator的编码格式 通过->编辑->选择编码 查看. 2)如果qt creator的编码格式 ...
 - 【Java每日一题】20170310
			
20170309问题解析请点击今日问题下方的“[Java每日一题]20170310”查看(问题解析在公众号首发,公众号ID:weknow619) package Mar2017; public cla ...
 - Javascript删除数组里的某个元素
			
删除array数组中的某个元素,首先需要确定需要删除元素的索引值. ? 1 2 3 4 5 6 7 var arr=[1,5,6,12,453,324]; function indexOf(val){ ...