• 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         }
    1. 当获取Time1时,因为缓存中不存在Time1,所以进入CreateEntry方法。
    2. 因为之前无任何操作,且DefaultCacheContextAccessor也未对_threadInstance初始化,所以_cacheContextAccessor.Current为null。
    3. 将新建的AcquireContext作为_cacheContextAccessor.Current,并调用acquire方法。
    4. Time1的acquire方法中又需要去缓存中取Time,因为不存在又进入CreateEntry方法,但这次不同的是_cacheContextAccessor.Current不为null。
    5. Time通过acquire方法创建了缓存值以及5秒过期的Token,并进入到PropagateTokens方法。
    6. 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         }

    这段代码主要做了三件事情:

    1. 一开始的时候它为每一个元素(每一个Folder)附加上了一个AcquireContext,即TaskWithAcquireContext既包含用来获取元素的folder => folder.AvailableExtensions().ToList()表达式还包含了一个ICacheContextAccessor,通过上面的分析可知,ICacheContextAccessor用于缓存中存在包含其它缓存的情况。
    2. 并行处理每一个元素(每一个Folder)调用Execute方法。
    3. 完成后针对每一个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委托。

    最后分析一下上面并行处理缓存的过程:

    1. CacheManager通过Key"AvailableExtensions"去查找缓存,当第一次查找时缓存中不存在"AvailableExtensions"这个Key,那么调用Cache的CreateEntry方法。
    2. 这时就会创建一个AcquireContext(包含当前Key和一个AddToken的Mointor),然后将带着这个Context去执行Acquire方法,而现在的Acquire方法就是包含并行处理的那个代理。
    3. 其实也就是执行_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源码。

  • 相关阅读:
    AC 自动机
    [P4735] 最大异或和
    [HNOI2006] 最短母串问题
    [SHOI2002] 取石子游戏
    [NOI2014] 动物园
    [BZOJ2839] 集合计数
    【Spark】object not serializable (class: A)
    【Hbase】Master startup cannot progress, in holding-pattern until region onlined.
    Hbase 各个角色的工作。
    hbase region均衡机制
  • 原文地址:https://www.cnblogs.com/selimsong/p/6014889.html
Copyright © 2020-2023  润新知