一步步实现一个基本的缓存模块·续, 添加Memcached调用实现
jusfr 原创,转载请注明来自博客园。
在之前的实现中,我们初步实现了一个缓存模块:包含一个基于Http请求的缓存实现,一个基于HttpRuntime.Cache进程级的缓存实现,但观察代码,会发现如下问题:
1. 有部分逻辑如 Boolean TryGet<T>(String key, out T entry) 的实现有重复现象,Do not repeat yourself 提醒我们这里可以改进;
2. 分区特性虽然实现了,但是使用了额外的接口承载,而大多数运用中,调用者无论是操作缓存项的创建还是过期,都不太关心分区参数 Region;的机制问题,计数和全部过期貌似不太现实,从这个接口派生恐怕不妥,怎么办?
3. IHttpRuntimeCacheProvider 接口中功能太多,本文要添加一个基于 Memcached 的缓存实现类,而 Memcached 天然不支持遍历等操作怎么办?
处理第1个问题,先梳理一下缓存获取即 GetOrCreate 逻辑,多数情况是这样的
1)尝试从某容器或客户端如 HttpContext.Current.Items、HttpRuntime.Cache、MemcachedClient 判断缓存是否存在及获取缓存对象;
2)缓存对象存在时进行类型对比,比如 id 已经被缓存成整型,现在新接口尝试将 Guid 类型写入,本文使用严格策略,该操作将抛出 InvalidOperationException 异常;
3)缓存不存在时,执行委托计算出缓存值,将其写入容器;
可以看出, GetOrCreate 将调用 TryGet 方法及 Overwrite 方法,我们可以使用抽象类,将前者写成具体实现,将后两者写成抽象方法,由具体子类去实现。
1 public interface ICacheProvider { 2 Boolean TryGet<T>(String key, out T entry); 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 entry); 6 void Expire(String key); 7 } 8 9 public abstract class CacheProvider : ICacheProvider { 10 protected virtual String BuildCacheKey(String key) { 11 return key; 12 } 13 14 protected abstract Boolean InnerTryGet(String key, out Object entry); 15 16 public virtual Boolean TryGet<T>(String key, out T entry) { 17 String cacheKey = BuildCacheKey(key); 18 Object cacheEntry; 19 Boolean exist = InnerTryGet(cacheKey, out cacheEntry); 20 if (exist) { 21 if (cacheEntry != null) { 22 if (!(cacheEntry is T)) { 23 throw new InvalidOperationException(String.Format("缓存项`[{0}]`类型错误, {1} or {2} ?", 24 key, cacheEntry.GetType().FullName, typeof(T).FullName)); 25 } 26 entry = (T)cacheEntry; 27 } 28 else { 29 entry = (T)((Object)null); 30 } 31 } 32 else { 33 entry = default(T); 34 } 35 return exist; 36 } 37 38 public virtual T GetOrCreate<T>(String key, Func<T> function) { 39 T entry; 40 if (TryGet(key, out entry)) { 41 return entry; 42 } 43 entry = function(); 44 Overwrite(key, entry); 45 return entry; 46 } 47 48 public virtual T GetOrCreate<T>(String key, Func<String, T> factory) { 49 T entry; 50 if (TryGet(key, out entry)) { 51 return entry; 52 } 53 entry = factory(key); 54 Overwrite(key, entry); 55 return entry; 56 } 57 58 public abstract void Overwrite<T>(String key, T value); 59 60 public abstract void Expire(String key); 61 }
抽象类 CacheProvider 的 InnerTryGet、Overwrite、Expire 是需要实现类来完成的,GetOrCreate 调用它们来完成核心逻辑;于是 HttpContextCacheProvider 的实现,逻辑在父类实现后,看起来非常简洁了:
1 public class HttpContextCacheProvider : CacheProvider, ICacheProvider { 2 private const String _prefix = "HttpContextCacheProvider_"; 3 protected override String BuildCacheKey(String key) { 4 return String.Concat(_prefix, key); 5 } 6 7 protected override Boolean InnerTryGet(String key, out Object entry) { 8 Boolean exist = false; 9 entry = null; 10 if (HttpContext.Current.Items.Contains(key)) { 11 exist = true; 12 entry = HttpContext.Current.Items[key]; 13 } 14 return exist; 15 } 16 17 public override void Overwrite<T>(String key, T entry) { 18 HttpContext.Current.Items[BuildCacheKey(key)] = entry; 19 } 20 21 public override void Expire(String key) { 22 HttpContext.Current.Items.Remove(BuildCacheKey(key)); 23 } 24 }
这里不准备为基于 HttpContext 的缓存提供太多特性,但基于 HttpRuntime.Cache 的缓存就需要像过期之类的功能,在实现之前先考虑问题2。
首先,既然用户没有必要甚至不知道分区存在,我们直接实现支持分区特性的子类好了;然后,计数与过期功能 HttpRuntime.Cache 支持但 Memcached 不,所以这部分功能需要从 IHttpRuntimeCacheProvider 中拆分出来,没错,扩展方法!于是拆分如下:
1 public interface IRegion { 2 String Region { get; } 3 } 4 5 public interface IHttpRuntimeCacheProvider : ICacheProvider { 6 T GetOrCreate<T>(String key, Func<T> function, TimeSpan slidingExpiration); 7 T GetOrCreate<T>(String key, Func<T> function, DateTime absoluteExpiration); 8 void Overwrite<T>(String key, T value, TimeSpan slidingExpiration); 9 void Overwrite<T>(String key, T value, DateTime absoluteExpiration); 10 }
其中IHttpRuntimeCacheProvider接口定义了带有过期参数的缓存操作方法,我们需要实现抽象方法与额外接口如下:
1 public class HttpRuntimeCacheProvider : CacheProvider, IHttpRuntimeCacheProvider, IRegion { 2 private static readonly Object _nullEntry = new Object(); 3 private String _prefix = "HttpRuntimeCacheProvider_"; 4 5 public virtual String Region { get; private set; } 6 7 public HttpRuntimeCacheProvider() { 8 } 9 10 public HttpRuntimeCacheProvider(String region) { 11 Region = region; 12 } 13 14 protected override bool InnerTryGet(String key, out object entry) { 15 entry = HttpRuntime.Cache.Get(key); 16 return entry != null; 17 } 18 19 protected override String BuildCacheKey(String key) { 20 //Region 为空将被当作 String.Empty 处理 21 return Region == null 22 ? String.Concat(_prefix, key) 23 : String.Concat(_prefix, Region, key); 24 } 25 26 private Object BuildCacheEntry<T>(T value) { 27 Object entry = value; 28 if (value == null) { 29 entry = _nullEntry; 30 } 31 return entry; 32 } 33 34 35 public T GetOrCreate<T>(String key, Func<T> function, TimeSpan slidingExpiration) { 36 T value; 37 if (TryGet<T>(key, out value)) { 38 return value; 39 } 40 value = function(); 41 Overwrite(key, value, slidingExpiration); 42 return value; 43 } 44 45 public T GetOrCreate<T>(String key, Func<T> function, DateTime absoluteExpiration) { 46 T value; 47 if (TryGet<T>(key, out value)) { 48 return value; 49 } 50 value = function(); 51 Overwrite(key, value, absoluteExpiration); 52 return value; 53 } 54 55 public override void Overwrite<T>(String key, T value) { 56 HttpRuntime.Cache.Insert(BuildCacheKey(key), BuildCacheEntry<T>(value)); 57 } 58 59 //slidingExpiration 时间内无访问则过期 60 public void Overwrite<T>(String key, T value, TimeSpan slidingExpiration) { 61 HttpRuntime.Cache.Insert(BuildCacheKey(key), BuildCacheEntry<T>(value), null, 62 Cache.NoAbsoluteExpiration, slidingExpiration); 63 } 64 65 //absoluteExpiration 时过期 66 public void Overwrite<T>(String key, T value, DateTime absoluteExpiration) { 67 HttpRuntime.Cache.Insert(BuildCacheKey(key), BuildCacheEntry<T>(value), null, 68 absoluteExpiration, Cache.NoSlidingExpiration); 69 } 70 71 public override void Expire(String key) { 72 HttpRuntime.Cache.Remove(BuildCacheKey(key)); 73 } 74 75 internal Boolean Hit(DictionaryEntry entry) { 76 return (entry.Key is String) 77 && ((String)entry.Key).StartsWith(BuildCacheKey(String.Empty)); 78 } 79 }
HttpRuntimeCacheProvider 暴露了一个 internal 修饰的方法,提供给扩展方法调用:
1 public static class HttpRuntimeCacheProviderExtensions { 2 3 public static void ExpireAll(this HttpRuntimeCacheProvider cacheProvider) { 4 var entries = HttpRuntime.Cache.OfType<DictionaryEntry>() 5 .Where(cacheProvider.Hit); 6 foreach (var entry in entries) { 7 HttpRuntime.Cache.Remove((String)entry.Key); 8 } 9 } 10 11 public static Int32 Count(this HttpRuntimeCacheProvider cacheProvider) { 12 return HttpRuntime.Cache.OfType<DictionaryEntry>() 13 .Where(cacheProvider.Hit).Count(); 14 } 15 16 public static String Dump(this HttpRuntimeCacheProvider cacheProvider) { 17 var builder = new StringBuilder(1024); 18 builder.AppendLine("--------------------HttpRuntimeCacheProvider.Dump--------------------------"); 19 builder.AppendFormat("EffectivePercentagePhysicalMemoryLimit: {0}\r\n", HttpRuntime.Cache.EffectivePercentagePhysicalMemoryLimit); 20 builder.AppendFormat("EffectivePrivateBytesLimit: {0}\r\n", HttpRuntime.Cache.EffectivePrivateBytesLimit); 21 builder.AppendFormat("Count: {0}\r\n", HttpRuntime.Cache.Count); 22 builder.AppendLine(); 23 var entries = HttpRuntime.Cache.OfType<DictionaryEntry>().Where(cacheProvider.Hit).OrderBy(de => de.Key); 24 foreach (var entry in entries) { 25 builder.AppendFormat("{0}\r\n {1}\r\n", entry.Key, entry.Value.GetType().FullName); 26 } 27 builder.AppendLine("--------------------HttpRuntimeCacheProvider.Dump--------------------------"); 28 Debug.WriteLine(builder.ToString()); 29 return builder.ToString(); 30 } 31 }
考虑到计数、全部过期等功能并不常用,所以这里基本实现功能,并未周全地考虑并发、效率问题;至此功能拆分完成,我们转入 Memcached 实现;
Memcached 客户端有相当多的C#实现,这里我选择了 EnyimMemcached,最新版本为2.12,见 https://github.com/enyim/EnyimMemcached 。与 HttpRuntimeCacheProvider 非常类似,从 CacheProvider 继承,实现 IHttpRuntimeCacheProvider, IRegion 接口,完成必要的逻辑即可。
1 public class MemcachedCacheProvider : CacheProvider, IHttpRuntimeCacheProvider, IRegion { 2 private static readonly MemcachedClient _client = new MemcachedClient("enyim.com/memcached"); 3 4 public String Region { get; private set; } 5 6 public MemcachedCacheProvider() 7 : this(String.Empty) { 8 } 9 10 public MemcachedCacheProvider(String region) { 11 Region = region; 12 } 13 14 protected override String BuildCacheKey(String key) { 15 return Region == null ? key : String.Concat(Region, "_", key); 16 } 17 18 protected override bool InnerTryGet(string key, out object entry) { 19 return _client.TryGet(key, out entry); 20 } 21 22 23 public T GetOrCreate<T>(String key, Func<T> function, TimeSpan slidingExpiration) { 24 T value; 25 if (TryGet<T>(key, out value)) { 26 return value; 27 } 28 value = function(); 29 Overwrite(key, value, slidingExpiration); 30 return value; 31 } 32 33 public T GetOrCreate<T>(String key, Func<T> function, DateTime absoluteExpiration) { 34 T value; 35 if (TryGet<T>(key, out value)) { 36 return value; 37 } 38 value = function(); 39 Overwrite(key, value, absoluteExpiration); 40 return value; 41 } 42 43 public override void Overwrite<T>(String key, T value) { 44 _client.Store(StoreMode.Set, BuildCacheKey(key), value); 45 } 46 47 //slidingExpiration 时间内无访问则过期 48 public void Overwrite<T>(String key, T value, TimeSpan slidingExpiration) { 49 _client.Store(StoreMode.Set, BuildCacheKey(key), value, slidingExpiration); 50 } 51 52 //absoluteExpiration 时过期 53 public void Overwrite<T>(String key, T value, DateTime absoluteExpiration) { 54 _client.Store(StoreMode.Set, BuildCacheKey(key), value, absoluteExpiration); 55 } 56 57 public override void Expire(String key) { 58 _client.Remove(BuildCacheKey(key)); 59 } 60 }
EnyimMemcached 天然支持空缓存项,另外过期时间会因为客户端与服务器时间不严格一致出现测试未通过的情况,它不推荐使用过多的 MemcachedClient 实例,所以此处写成单例形式,另外如何配置等问题,请翻看项目的 Github,本文只使用了最基本的配置,见源代码,更多设置项及解释见 Github 。
需要注意的是,EnyimMemcached 处理的自定义对象需要使用 [Serializable] 修饰,不然操作无效且不报错,存在产生重大Bug的可能;
最后是工厂类 CacheProviderFactory 的实现,这里从类库项目中排除掉了,即可以是形如 #if DEBUG 类的条件编译,也可以按配置文件来,个人感觉应该在应用中提供统一的入口功能即可。另外 Memcached 的特性本文使用有限,所以未从新接口派生,各位看自己需求扩展既是。
补图:
包含测试用例的源码见 Github , jusfr 原创,转载请注明来自博客园。