从Orchard学到的东西--第六篇 CacheManager 2
Posted 7m鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从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的缓存是否过期。
讲到这先看一个例子:
1 private void CacheTest() 2 { 3 var time1 = _cacheManager.Get("Time1", ctx => { 4 ctx.Monitor(_clock.When(TimeSpan.FromHours(1))); 5 return GetStringFromCache(); 6 }); 7 Thread.Sleep(1000); 8 var time2 = _cacheManager.Get("Time1", ctx => { 9 ctx.Monitor(_clock.When(TimeSpan.FromHours(1))); 10 return GetStringFromCache(); 11 }); 12 } 13 14 private string GetStringFromCache() 15 { 16 return _cacheManager.Get("Time", ctx => { 17 ctx.Monitor(_clock.When(TimeSpan.FromSeconds(5))); 18 return DateTime.Now.ToString(); 19 }); 20 }
Key为Time1的缓存包含了另一个Key为Time的缓存,并且Time1的有效时间为1小时而Time的有效时间只有5秒。那么问题来了Time1必须等待1小时再去更新吗?但是Time的值已经更新N次了?先看一下调试结果:
发现两次结果不一致(因为断点暂停所以时间超过5秒)。
看一下缓存中的实际内容:
Time:
Time1:
有没有发现Time1有两个Token?
并且第二个的IsCurrent属性已经为false了。Why?
让我们再回到Cache的代码:
1 private CacheEntry AddEntry(TKey k, Func<AcquireContext<TKey>, TResult> acquire) { 2 var entry = CreateEntry(k, acquire); 3 PropagateTokens(entry); 4 return entry; 5 } 6 7 8 private void PropagateTokens(CacheEntry entry) { 9 // Bubble up volatile tokens to parent context 10 if (_cacheContextAccessor.Current != null) { 11 foreach (var token in entry.Tokens) 12 _cacheContextAccessor.Current.Monitor(token); 13 } 14 } 15 16 17 private CacheEntry CreateEntry(TKey k, Func<AcquireContext<TKey>, TResult> acquire) { 18 var entry = new CacheEntry(); 19 var context = new AcquireContext<TKey>(k, entry.AddToken); 20 21 IAcquireContext parentContext = null; 22 try { 23 // Push context 24 parentContext = _cacheContextAccessor.Current; 25 _cacheContextAccessor.Current = context; 26 27 entry.Result = acquire(context); 28 } 29 finally { 30 // Pop context 31 _cacheContextAccessor.Current = parentContext; 32 } 33 entry.CompactTokens(); 34 return entry; 35 }
- 当获取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,它又有什么作用呢?
先看一个实际使用的例子:
1 public IEnumerable<ExtensionDescriptor> AvailableExtensions() { 2 return _cacheManager.Get("AvailableExtensions", true, ctx => 3 _parallelCacheContext 4 .RunInParallel(_folders, folder => folder.AvailableExtensions().ToList()) 5 .SelectMany(descriptors => descriptors) 6 .ToReadOnlyCollection()); 7 }
这个例子根据代码表面意思来看是以并行的方式将每个folder下的拓展信息获取出来。
看一下RunInParallel的实现细节:
1 public IEnumerable<TResult> RunInParallel<T, TResult>(IEnumerable<T> source, Func<T, TResult> selector) { 2 if (Disabled) { 3 return source.Select(selector); 4 } 5 else { 6 // Create tasks that capture the current thread context 7 var tasks = source.Select(item => this.CreateContextAwareTask(() => selector(item))).ToList(); 8 9 // Run tasks in parallel and combine results immediately 10 var result = tasks 11 .AsParallel() // prepare for parallel execution 12 .AsOrdered() // preserve initial enumeration order 13 .Select(task => task.Execute()) // prepare tasks to run in parallel 14 .ToArray(); // force evaluation 15 16 // Forward tokens collected by tasks to the current context 17 foreach (var task in tasks) { 18 task.Finish(); 19 } 20 return result; 21 } 22 } 23 24 /// <summary> 25 /// Create a task that wraps some piece of code that implictly depends on the cache context. 26 /// The return task can be used in any execution thread (e.g. System.Threading.Tasks). 27 /// </summary> 28 public ITask<T> CreateContextAwareTask<T>(Func<T> function) { 29 return new TaskWithAcquireContext<T>(_cacheContextAccessor, function); 30 } 31 32 public class TaskWithAcquireContext<T> : ITask<T> { 33 private readonly ICacheContextAccessor _cacheContextAccessor; 34 private readonly Func<T> _function; 35 private IList<IVolatileToken> _tokens; 36 37 public TaskWithAcquireContext(ICacheContextAccessor cacheContextAccessor, Func<T> function) { 38 _cacheContextAccessor = cacheContextAccessor; 39 _function = function; 40 } 41 }
这段代码主要做了三件事情:
- 一开始的时候它为每一个元素(每一个Folder)附加上了一个AcquireContext,即TaskWithAcquireContext既包含用来获取元素的folder => folder.AvailableExtensions().ToList()表达式还包含了一个ICacheContextAccessor,通过上面的分析可知,ICacheContextAccessor用于缓存中存在包含其它缓存的情况。
- 并行处理每一个元素(每一个Folder)调用Execute方法。
- 完成后针对每一个Task调用Finish方法。
接下来看一下Execute和Finish方法的实现:
1 /// <summary> 2 /// Execute task and collect eventual volatile tokens 3 /// </summary> 4 public T Execute() { 5 IAcquireContext parentContext = _cacheContextAccessor.Current; 6 try { 7 // Push context 8 if (parentContext == null) { 9 _cacheContextAccessor.Current = new SimpleAcquireContext(AddToken); 10 } 11 12 // Execute lambda 13 return _function(); 14 } 15 finally { 16 // Pop context 17 if (parentContext == null) { 18 _cacheContextAccessor.Current = parentContext; 19 } 20 } 21 } 22 23 /// <summary> 24 /// Return tokens collected during task execution 25 /// </summary> 26 public IEnumerable<IVolatileToken> Tokens { 27 get { 28 if (_tokens == null) 29 return Enumerable.Empty<IVolatileToken>(); 30 return _tokens; 31 } 32 } 33 34 public void Dispose() { 35 Finish(); 36 } 37 38 /// <summary> 39 /// Forward collected tokens to current cache context 40 /// </summary> 41 public void Finish() { 42 var tokens = _tokens; 43 _tokens = null; 44 if (_cacheContextAccessor.Current != null && tokens != null) { 45 foreach (var token in tokens) { 46 _cacheContextAccessor.Current.Monitor(token); 47 } 48 } 49 } 50 51 private void AddToken(IVolatileToken token) { 52 if (_tokens == null) 53 _tokens = new List<IVolatileToken>(); 54 _tokens.Add(token); 55 } 56 }
是否与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的主要内容,如果未能解决你的问题,请参考以下文章