标签:style blog http io ar color os 使用 sp
1. 前言
2. 请求级别缓存
2.1 多线程
3. 进程级别缓存
3.1 分区与计数
3.2 可空缓存值
3.3 封装与集成
4. 小结
1. 前言
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() 方法则同样需要了。测试用例:
1 [TestClass] 2 public class HttpContextCacheProviderTest { 3 [TestInitialize] 4 public void Initialize() { 5 HttpContext.Current = new HttpContext(new HttpRequest(null, "http://localhost", null), new HttpResponse(null)); 6 } 7 8 [TestMethod] 9 public void NullValue() { 10 var key = "key-null"; 11 HttpContext.Current.Items.Add(key, null); 12 Assert.IsTrue(HttpContext.Current.Items.Contains(key)); 13 Assert.IsNull(HttpContext.Current.Items[key]); 14 } 15 16 [TestMethod] 17 public void ValueType() { 18 var key = "key-guid"; 19 ICacheProvider cache = new HttpContextCacheProvider(); 20 var id1 = Guid.NewGuid(); 21 var id2 = cache.GetOrCreate(key, () => id1); 22 Assert.AreEqual(id1, id2); 23 24 cache.Expire(key); 25 Guid id3; 26 var exist = cache.TryGet(key, out id3); 27 Assert.IsFalse(exist); 28 Assert.AreNotEqual(id1, id3); 29 Assert.AreEqual(id3, Guid.Empty); 30 } 31 }
引用类型测试用例忽略。
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 类似,这里贴出缓存过期的测试:
1 public class HttpRuntimeCacheProviderTest { 2 [TestMethod] 3 public void GetOrCreateWithAbsoluteExpirationTest() { 4 var key = Guid.NewGuid().ToString(); 5 var val = Guid.NewGuid(); 6 7 IHttpRuntimeCacheProvider cacheProvider = new HttpRuntimeCacheProvider(); 8 var result = cacheProvider.GetOrCreate<Guid>(key, () => val, DateTime.UtcNow.AddSeconds(2D)); 9 Assert.AreEqual(result, val); 10 11 var exist = cacheProvider.TryGet<Guid>(key, out val); 12 Assert.IsTrue(exist); 13 Assert.AreEqual(result, val); 14 15 Thread.Sleep(2000); 16 exist = cacheProvider.TryGet<Guid>(key, out val); 17 Assert.IsFalse(exist); 18 Assert.AreEqual(val, Guid.Empty); 19 } 20 21 [TestMethod] 22 public void ExpireAllTest() { 23 var key = Guid.NewGuid().ToString(); 24 var val = Guid.NewGuid(); 25 26 IHttpRuntimeCacheProvider cacheProvider = new HttpRuntimeCacheProvider(); 27 var result = cacheProvider.GetOrCreate<Guid>(key, () => val); 28 Assert.AreEqual(result, val); 29 30 cacheProvider.ExpireAll(); 31 Guid val2; 32 var exist = cacheProvider.TryGet<Guid>(key, out val2); 33 Assert.IsFalse(exist); 34 Assert.AreEqual(val2, Guid.Empty); 35 } 36 }
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 的操作:
1 [TestClass] 2 public class HttpRuntimeRegionCacheProviderTest { 3 [TestMethod] 4 public void ValueType() { 5 var key = "key-guid"; 6 IHttpRuntimeCacheProvider cache1 = new HttpRuntimeRegionCacheProvider("Region1"); 7 var id1 = cache1.GetOrCreate(key, Guid.NewGuid); 8 9 IHttpRuntimeCacheProvider cache2 = new HttpRuntimeRegionCacheProvider("Region2"); 10 var id2 = cache2.GetOrCreate(key, Guid.NewGuid); 11 Assert.AreNotEqual(id1, id2); 12 13 cache1.ExpireAll(); 14 Assert.AreEqual(cache1.Count, 0); 15 Assert.AreEqual(cache2.Count, 1); 16 } 17 }
至此一个基本的缓存模块已经完成;
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 }
测试用例:
1 [TestClass] 2 public class HttpRuntimeCacheProviderTest { 3 [TestMethod] 4 public void NullCacheErrorTest() { 5 var key = "key-null"; 6 Person person = null; 7 IHttpRuntimeCacheProvider cacheProvider = new HttpRuntimeCacheProvider(false); 8 try { 9 cacheProvider.GetOrCreate<Person>(key, () => person); //error 10 Assert.Fail(); 11 } 12 catch (Exception ex) { 13 Assert.IsTrue(ex is InvalidOperationException); 14 } 15 16 Person person2; 17 var exist = cacheProvider.TryGet(key, out person2); 18 Assert.IsFalse(exist); 19 Assert.AreEqual(person2, null); 20 } 21 22 [TestMethod] 23 public void NullableCacheTest() { 24 var key = "key-nullable"; 25 Person person = null; 26 IHttpRuntimeCacheProvider cacheProvider = new HttpRuntimeCacheProvider(true); 27 cacheProvider.GetOrCreate<Person>(key, () => person); 28 Person person2; 29 var exist = cacheProvider.TryGet(key, out person2); 30 Assert.IsTrue(exist); 31 Assert.AreEqual(person2, null); 32 } 33 34 class Person { 35 public Int32 Id { get; set; } 36 public String Name { get; set; } 37 } 38 }
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 原创,转载请注明来自博客园 。
标签:style blog http io ar color os 使用 sp
原文地址:http://www.cnblogs.com/Jusfr/p/4150954.html