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

注意后续代码及改进见后后文及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     }
View Code

相关文章: