注意后续代码及改进见后后文及github,文章上的并没有更新。
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() 方法则同样需要了。测试用例:
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 }