缓存模块
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 Code3.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 Code3.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 原创,转载请注明来自博客园 。
