zoukankan      html  css  js  c++  java
  • 缓存模块

    缓存模块

    一步步实现一个基本的缓存模块

        1. 前言
        2.  请求级别缓存
        2.1 多线程
        3.  进程级别缓存
        3.1 分区与计数
        3.2 可空缓存值
        3.3 封装与集成
        4.  小结

    1. 前言

    • 面向读者:初、中级用户;
    • 涉及知识:HttpContext、HttpRuime.Cache、DictionaryEntry、Unit Test等;
    • 文章目的:这里的内容不会涉及 Memcached、Redies 等进程外缓存的使用,只针对包含WEB应用的常见场景,实现一个具有线程安全、分区、过期特性的缓存模块,略微提及DI等内容。
    • jusfr 原创,转载请注明来自博客园


    2.  请求级别缓存

    如果需要线程安全地存取数据,System.Collections.Concurrent 命名空间下的像 ConcurrentDictionary 等实现是首选;更复杂的特性像过期策略、文件依赖等就需要其他实现了。ASP.NET中的HttpContext.Current.Items 常常被用作自定义数据容器,注入工具像Unity、Autofac 等便借助自定义 HttpModule 将容器挂接在 HttpContext.Current 上以进行生命周期管理。

    基本接口 ICacheProvider,请求级别的缓存从它定义,考虑到请求级别缓存的运用场景有限,故只定义有限特性;

    复制代码
    1     public interface ICacheProvider {
    2         Boolean TryGet<T>(String key, out T value);
    3         T GetOrCreate<T>(String key, Func<T> function);
    4         T GetOrCreate<T>(String key, Func<String, T> factory);
    5         void Overwrite<T>(String key, T value);
    6         void Expire(String key);
    7     }
    复制代码

    HttpContext.Current.Items 从 IDictionary 定义,存储 Object-Object 键值对,出于便利与直观,ICacheProvider 只接受String类型缓存键,故HttpContextCacheProvider内部使用 BuildCacheKey(String key) 方法生成真正缓存键以避免键值重复;

    同时 HashTable 可以存储空引用作为缓存值,故 TryGet() 方法先进行 Contains() 判断存在与否,再进行类型判断,避免缓存键重复使用;  

    复制代码
     1 public class HttpContextCacheProvider : ICacheProvider {
     2         protected virtual String BuildCacheKey(String key) {
     3             return String.Concat("HttpContextCacheProvider_", key);
     4         }
     5 
     6         public Boolean TryGet<T>(String key, out T value) {
     7             key = BuildCacheKey(key);
     8             Boolean exist = false;
     9             if (HttpContext.Current.Items.Contains(key)) {
    10                 exist = true;
    11                 Object entry = HttpContext.Current.Items[key];
    12                 if (entry != null && !(entry is T)) {
    13                     throw new InvalidOperationException(String.Format("缓存项`[{0}]`类型错误, {1} or {2} ?",
    14                         key, entry.GetType().FullName, typeof(T).FullName));
    15                 }
    16                 value = (T)entry;
    17             }
    18             else {
    19                 value = default(T);
    20             }
    21             return exist;
    22         }
    23 
    24         public T GetOrCreate<T>(String key, Func<T> function) {
    25             T value;
    26             if (TryGet(key, out value)) {
    27                 return value;
    28             }
    29             value = function();
    30             Overwrite(key, value);
    31             return value;
    32         }
    33 
    34         public T GetOrCreate<T>(String key, Func<String, T> factory) {
    35             T value;
    36             if (TryGet(key, out value)) {
    37                 return value;
    38             }
    39             value = factory(key);
    40             Overwrite(key, value);
    41             return value;
    42         }
    43 
    44         public void Overwrite<T>(String key, T value) {
    45             key = BuildCacheKey(key);
    46             HttpContext.Current.Items[key] = value;
    47         }
    48 
    49         public void Expire(String key) {
    50             key = BuildCacheKey(key);
    51             HttpContext.Current.Items.Remove(key);
    52         }
    53     }
    复制代码

    这里使用了 Func<T> 委托的运用,合并查询、判断和添加缓存项的操作以简化接口调用;如果用户期望不同类型缓存值可以存储到相同的 key 上,则需要重新定义 BuildCacheKey() 方法将缓存值类型作为参数参与生成缓存键,此时 Expire() 方法则同样需要了。测试用例:

     View Code

    引用类型测试用例忽略。


    2.1 多线程

    异步等情况下,HttpContext.Current并非无处不在,故异步等情况下 HttpContextCacheProvider 的使用可能抛出空引用异常,需要被处理,对此园友有过思考 ,这里贴上A大的方案 ,有需求的读者请按图索骥。

    3.  进程级别缓存

    HttpRuntime.Cache 定义在 System.Web.dll 中,System.Web 命名空间下,实际上是可以使用在非 Asp.Net 应用里的;另外 HttpContext 对象包含一个 Cache 属性,它们的关系可以阅读 HttpContext.Cache 和 HttpRuntime.Cache

    HttpRuntime.Cache 为 System.Web.Caching.Cache 类型,支持滑动/绝对时间过期策略、支持缓存优先级、缓存更新/过期回调、基于文件的缓存依赖项等,功能十分强大,这里借用少数特性来实现进程级别缓存,更多文档请自行检索。

    从 ICacheProvider 定义 IHttpRuntimeCacheProvider,添加相对过期与绝对过期、添加批量的缓存过期接口 ExpireAll();

    复制代码
    1     public interface IHttpRuntimeCacheProvider : ICacheProvider {
    2         T GetOrCreate<T>(String key, Func<T> function, TimeSpan slidingExpiration);
    3         T GetOrCreate<T>(String key, Func<T> function, DateTime absoluteExpiration);
    4         void Overwrite<T>(String key, T value, TimeSpan slidingExpiration);
    5         void Overwrite<T>(String key, T value, DateTime absoluteExpiration);
    6         void ExpireAll();
    7     }
    复制代码

    System.Web.Caching.Cache 只继承 IEnumerable,内部使用 DictionaryEntry 存储Object-Object 键值对,但 HttpRuntime.Cache 只授受字符串类型缓存键及非空缓存值,关于空引用缓存值的问题,我们在3.2中讨论;

    故 TryGet() 与 HttpContextCacheProvider.TryGet() 具有显著差异,前者需要拿出值来进行非空判断,后者则是使用 IDictionary.Contains() 方法;

    除了 TryGet() 方法与过期过期参数外的差异外,接口实现与 HttpContextCacheProvider 类似;

    复制代码
     1     public class HttpRuntimeCacheProvider : IHttpRuntimeCacheProvider {
     2         private static readonly Object _sync = new Object();
     3 
     4         protected virtual String BuildCacheKey(String key) {
     5             return String.Concat("HttpRuntimeCacheProvider_", key);
     6         }
     7 
     8         public Boolean TryGet<T>(String key, out T value) {
     9             key = BuildCacheKey(key);
    10             Boolean exist = false;
    11             Object entry = HttpRuntime.Cache.Get(key);
    12             if (entry != null) {
    13                 exist = true;
    14                 if (!(entry is T)) {
    15                     throw new InvalidOperationException(String.Format("缓存项[{0}]类型错误, {1} or {2} ?",
    16                         key, entry.GetType().FullName, typeof(T).FullName));
    17                 }
    18                 value = (T)entry;
    19             }
    20             else {
    21                 value = default(T);
    22             }
    23             return exist;
    24         }
    25 
    26         public T GetOrCreate<T>(String key, Func<String, T> factory) {
    27             T result;
    28             if (TryGet<T>(key, out result)) {
    29                 return result;
    30             }
    31             result = factory(key);
    32             Overwrite(key, result);
    33             return result;
    34         }
    35 
    36         public T GetOrCreate<T>(String key, Func<T> function) {
    37             T result;
    38             if (TryGet<T>(key, out result)) {
    39                 return result;
    40             }
    41             result = function();
    42             Overwrite(key, result);
    43             return result;
    44         }
    45 
    46 
    47         public T GetOrCreate<T>(String key, Func<T> function, TimeSpan slidingExpiration) {
    48             T result;
    49             if (TryGet<T>(key, out result)) {
    50                 return result;
    51             }
    52             result = function();
    53             Overwrite(key, result, slidingExpiration);
    54             return result;
    55         }
    56 
    57         public T GetOrCreate<T>(String key, Func<T> function, DateTime absoluteExpiration) {
    58             T result;
    59             if (TryGet<T>(key, out result)) {
    60                 return result;
    61             }
    62             result = function();
    63             Overwrite(key, result, absoluteExpiration);
    64             return result;
    65         }
    66 
    67         public void Overwrite<T>(String key, T value) {
    68             HttpRuntime.Cache.Insert(BuildCacheKey(key), value);
    69         }
    70 
    71         //slidingExpiration 时间内无访问则过期
    72         public void Overwrite<T>(String key, T value, TimeSpan slidingExpiration) {
    73             HttpRuntime.Cache.Insert(BuildCacheKey(key), value, null,
    74                 Cache.NoAbsoluteExpiration, slidingExpiration);
    75         }
    76 
    77         //absoluteExpiration 绝对时间过期
    78         public void Overwrite<T>(String key, T value, DateTime absoluteExpiration) {
    79             HttpRuntime.Cache.Insert(BuildCacheKey(key), value, null,
    80                 absoluteExpiration, Cache.NoSlidingExpiration);
    81         }
    82 
    83         public void Expire(String key) {
    84             HttpRuntime.Cache.Remove(BuildCacheKey(key));
    85         }
    86 
    87         public void ExpireAll() {
    88             lock (_sync) {
    89                 var entries = HttpRuntime.Cache.OfType<DictionaryEntry>()
    90                     .Where(entry => (entry.Key is String) && ((String)entry.Key).StartsWith("HttpRuntimeCacheProvider_"));
    91                 foreach (var entry in entries) {
    92                     HttpRuntime.Cache.Remove((String)entry.Key);
    93                 }
    94             }
    95         }
    96     }
    复制代码

    测试用例与 HttpContextCacheProviderTest 类似,这里贴出缓存过期的测试:  

     View Code

    3.1 分区与计数

    缓存分区是常见需求,缓存用户A、用户B的认证信息可以拿用户标识作为缓存键,但每个用户分别有一整套包含授权的其他数据时,为创建以用户分区的缓存应该是更好的选择;
    常规的想法是为缓存添加类似 `Region` 或 `Partition`的参数,个人觉得这不是很好的实践,因为接口被修改,同时过多的参数非常让人困惑;

    读者可能对前文中 BuildCacheKey() 方法被 protected virtual 修饰觉得很奇怪,是的,个人觉得定义新的接口,配合从缓存Key的生成算法作文章来分区貌似比较巧妙,也迎合依赖注册被被广泛使用的现状;

    分区的进程级别缓存定义,只需多出一个属性:

    1     public interface IHttpRuntimeRegionCacheProvider : IHttpRuntimeCacheProvider {
    2         String Region { get; }
    3     }

    分区的缓存实现,先为 IHttpRuntimeCacheProvider 添加计数,然后重构HttpRuntimeCacheProvider,提取出过滤算法,接着重写 BuildCacheKey() 方法的实现,使不同分区的生成不同的缓存键,缓存项操作方法无须修改;  

    复制代码
     1 public interface IHttpRuntimeCacheProvider : ICacheProvider {
     2         ...
     3         Int32 Count { get; }
     4     }
     5 
     6      public class HttpRuntimeCacheProvider : IHttpRuntimeCacheProvider {
     7         ...
     8         protected virtual Boolean Hit(DictionaryEntry entry) {
     9             return (entry.Key is String) && ((String)entry.Key).StartsWith("HttpRuntimeCacheProvider_");
    10         }
    11 
    12         public void ExpireAll() {
    13             lock (_sync) {
    14                 var entries = HttpRuntime.Cache.OfType<DictionaryEntry>().Where(Hit);
    15                 foreach (var entry in entries) {
    16                     HttpRuntime.Cache.Remove((String)entry.Key);
    17                 }
    18             }
    19         }
    20 
    21         public Int32 Count {
    22             get {
    23                 lock (_sync) {
    24                     return HttpRuntime.Cache.OfType<DictionaryEntry>().Where(Hit).Count();
    25                 }
    26             }
    27         }
    28     }
    29 
    30     public class HttpRuntimeRegionCacheProvider : HttpRuntimeCacheProvider, IHttpRuntimeRegionCacheProvider {
    31         private String _prefix;
    32         public virtual String Region { get; private set; }
    33 
    34         private String GetPrifix() {
    35             if (_prefix == null) {
    36                 _prefix = String.Concat("HttpRuntimeRegionCacheProvider_", Region, "_");
    37             }
    38             return _prefix;
    39         }
    40 
    41         public HttpRuntimeRegionCacheProvider(String region)  {
    42             Region = region;
    43         }
    44 
    45         protected override String BuildCacheKey(String key) {
    46             //Region 为空将被当作  String.Empty 处理
    47             return String.Concat(GetPrifix(), base.BuildCacheKey(key));
    48         }
    49 
    50         protected override Boolean Hit(DictionaryEntry entry) {
    51             return (entry.Key is String) && ((String)entry.Key).StartsWith(GetPrifix());
    52         }
    53     }
    复制代码

    测试用例示例了两个分区缓存对相同 key 的操作: 

     View Code

    至此一个基本的缓存模块已经完成;

    3.2 可空缓存值

    前文提及过,HttpRuntime.Cache 不授受空引用作为缓存值,与 HttpContext.Current.Items表现不同,另一方面实际需求中,空值作为字典的值仍然是有意义,此处给出一个支持空缓存值的实现;

    HttpRuntime.Cache 断然是不能把 null 存入的,查看 HttpRuntimeCacheProvider.TryGet() 方法,可知 HttpRuntime.Cache.Get() 获取的总是 Object 类型,思路可以这样展开:

    1) 添加缓存时进行判断,如果非空,常规处理,否则把用一个特定的自定义对象存入;
    2) 取出缓存时进行判断,如果为特定的自定义对象,返回 null;

    为 HttpRuntimeCacheProvider 的构造函数添加可选参数,TryGet() 加入 null 判断逻辑;添加方法 BuildCacheEntry(),替换空的缓存值为 _nullEntry,其他方法不变;  

    复制代码
      1 public class HttpRuntimeCacheProvider : IHttpRuntimeCacheProvider {
      2         private static readonly Object _sync = new Object();
      3         private static readonly Object _nullEntry = new Object();
      4         private Boolean _supportNull;
      5 
      6         public HttpRuntimeCacheProvider(Boolean supportNull = false) {
      7             _supportNull = supportNull;
      8         }
      9 
     10         protected virtual String BuildCacheKey(String key) {
     11             return String.Concat("HttpRuntimeCacheProvider_", key);
     12         }
     13 
     14         protected virtual Object BuildCacheEntry<T>(T value) {
     15             Object entry = value;
     16             if (value == null) {
     17                 if (_supportNull) {
     18                     entry = _nullEntry;
     19                 }
     20                 else {
     21                     throw new InvalidOperationException(String.Format("Null cache item not supported, try ctor with paramter 'supportNull = true' "));
     22                 }
     23             }
     24             return entry;
     25         }
     26 
     27         public Boolean TryGet<T>(String key, out T value) {
     28             Object entry = HttpRuntime.Cache.Get(BuildCacheKey(key));
     29             Boolean exist = false;
     30             if (entry != null) {
     31                 exist = true;
     32                 if (!(entry is T)) {
     33                     if (_supportNull && !(entry == _nullEntry)) {
     34                         throw new InvalidOperationException(String.Format("缓存项`[{0}]`类型错误, {1} or {2} ?",
     35                             key, entry.GetType().FullName, typeof(T).FullName));
     36                     }
     37                     value = (T)((Object)null);
     38                 }
     39                 else {
     40                     value = (T)entry;
     41                 }
     42             }
     43             else {
     44                 value = default(T);
     45             }
     46             return exist;
     47         }
     48 
     49         public T GetOrCreate<T>(String key, Func<String, T> factory) {
     50             T value;
     51             if (TryGet<T>(key, out value)) {
     52                 return value;
     53             }
     54             value = factory(key);
     55             Overwrite(key, value);
     56             return value;
     57         }
     58 
     59         public T GetOrCreate<T>(String key, Func<T> function) {
     60             T value;
     61             if (TryGet<T>(key, out value)) {
     62                 return value;
     63             }
     64             value = function();
     65             Overwrite(key, value);
     66             return value;
     67         }
     68 
     69         public T GetOrCreate<T>(String key, Func<T> function, TimeSpan slidingExpiration) {
     70             T value;
     71             if (TryGet<T>(key, out value)) {
     72                 return value;
     73             }
     74             value = function();
     75             Overwrite(key, value, slidingExpiration);
     76             return value;
     77         }
     78 
     79         public T GetOrCreate<T>(String key, Func<T> function, DateTime absoluteExpiration) {
     80             T value;
     81             if (TryGet<T>(key, out value)) {
     82                 return value;
     83             }
     84             value = function();
     85             Overwrite(key, value, absoluteExpiration);
     86             return value;
     87         }
     88 
     89         public void Overwrite<T>(String key, T value) {
     90             HttpRuntime.Cache.Insert(BuildCacheKey(key), BuildCacheEntry<T>(value));
     91         }
     92 
     93         //slidingExpiration 时间内无访问则过期
     94         public void Overwrite<T>(String key, T value, TimeSpan slidingExpiration) {
     95             HttpRuntime.Cache.Insert(BuildCacheKey(key), BuildCacheEntry<T>(value), null,
     96                 Cache.NoAbsoluteExpiration, slidingExpiration);
     97         }
     98 
     99         //absoluteExpiration 时过期
    100         public void Overwrite<T>(String key, T value, DateTime absoluteExpiration) {
    101             HttpRuntime.Cache.Insert(BuildCacheKey(key), BuildCacheEntry<T>(value), null,
    102                 absoluteExpiration, Cache.NoSlidingExpiration);
    103         }
    104 
    105         public void Expire(String key) {
    106             HttpRuntime.Cache.Remove(BuildCacheKey(key));
    107         }
    108 
    109         protected virtual Boolean Hit(DictionaryEntry entry) {
    110             return (entry.Key is String) && ((String)entry.Key).StartsWith("HttpRuntimeCacheProvider_");
    111         }
    112 
    113         public void ExpireAll() {
    114             lock (_sync) {
    115                 var entries = HttpRuntime.Cache.OfType<DictionaryEntry>().Where(Hit);
    116                 foreach (var entry in entries) {
    117                     HttpRuntime.Cache.Remove((String)entry.Key);
    118                 }
    119             }
    120         }
    121 
    122         public Int32 Count {
    123             get {
    124                 lock (_sync) {
    125                     return HttpRuntime.Cache.OfType<DictionaryEntry>().Where(Hit).Count();
    126                 }
    127             }
    128         }
    129     }
    复制代码

    然后是分区缓存需要修改构造函数:

    复制代码
     1     public HttpRuntimeRegionCacheProvider(String region)
     2             : base(false) {
     3             Region = region;
     4         }
     5 
     6         public HttpRuntimeRegionCacheProvider(String region, Boolean supportNull)
     7             : base(supportNull) {
     8             Region = region;
     9         }
    10         ...
    11     }
    复制代码

    测试用例: 

     View Code

    3.3 封装与集成

    多数情况下我们不需要暴露实现和手动创建上文所提各种 CacheProvider,实践中它们被 internal 修饰,再配合工厂类使用:  

    复制代码
     1 public static class CacheProviderFacotry {
     2         public static ICacheProvider GetHttpContextCache() {
     3             return new HttpContextCacheProvider();
     4         }
     5 
     6         public static IHttpRuntimeCacheProvider GetHttpRuntimeCache(Boolean supportNull = false) {
     7             return new HttpRuntimeCacheProvider(supportNull);
     8         }
     9 
    10         public static IHttpRuntimeRegionCacheProvider GetHttpRuntimeRegionCache(String region, Boolean supportNull = false) {
    11             return new HttpRuntimeRegionCacheProvider(region, supportNull);
    12         }
    13 
    14         public static IHttpRuntimeRegionCacheProvider Region(this IHttpRuntimeCacheProvider runtimeCacheProvider, String region, Boolean supportNull = false) {
    15             return GetHttpRuntimeRegionCache(region, supportNull);
    16         }
    17     }
    复制代码

    然后在依赖注入中的声明如下,这里是 Autofac 下的组件注册:     

    复制代码
    1  ...
    2             //请求级别缓存, 使用 HttpContext.Current.Items 作为容器
    3             builder.Register(ctx => CacheProviderFacotry.GetHttpContextCache()).As<ICacheProvider>().InstancePerLifetimeScope();
    4             //进程级别缓存, 使用 HttpRuntime.Cache 作为容器
    5             builder.RegisterInstance(CacheProviderFacotry.GetHttpRuntimeCache()).As<IRuntimeCacheProvider>().ExternallyOwned();
    6             //进程级别且隔离的缓存, 若出于key算法唯一考虑而希望加入上下文件信息, 则仍然需要 CacheModule 类的实现
    7             builder.Register(ctx => CacheProviderFacotry.GetHttpRuntimeRegionCache(/*... 分区依据 ...*/))
    8                 .As<IRuntimeRegionCacheProvider>().InstancePerLifetimeScope();
    9         ...
    复制代码

    4. 小结

    本文简单探讨了一个具有线程安全、分区、过期特性缓存模块的实现过程,只使用了HttpRuntime.Cache的有限特性,有更多需求的同学可以自行扩展;见解有限,谬误之处还请园友指正。

    园友Jusfr 原创,转载请注明来自博客园  。

     
     
    标签: .Net Cache
  • 相关阅读:
    Torchkeras,一个源码不足300行的深度学习框架
    【知乎】语义分割该如何走下去?
    【SDOI2017】天才黑客(前后缀优化建图 & 最短路)
    【WC2014】紫荆花之恋(替罪羊重构点分树 & 平衡树)
    【SDOI2017】相关分析(线段树)
    【学习笔记】分治法最短路小结
    【CH 弱省互测 Round #1 】OVOO(可持久化可并堆)
    【学习笔记】K 短路问题详解
    【学习笔记】浅析平衡树套线段树 & 带插入区间K小值
    【APIO2020】交换城市(Kruskal重构树)
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/4151928.html
Copyright © 2011-2022 走看看