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 原創,轉載請注明來自博客園。