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. public T GetOrCreate<T>(String key, Func<T> function, TimeSpan slidingExpiration) {
  35. T value;
  36. if (TryGet<T>(key, out value)) {
  37. return value;
  38. }
  39. value = function();
  40. Overwrite(key, value, slidingExpiration);
  41. return value;
  42. }
  43.  
  44. public T GetOrCreate<T>(String key, Func<T> function, DateTime absoluteExpiration) {
  45. T value;
  46. if (TryGet<T>(key, out value)) {
  47. return value;
  48. }
  49. value = function();
  50. Overwrite(key, value, absoluteExpiration);
  51. return value;
  52. }
  53.  
  54. public override void Overwrite<T>(String key, T value) {
  55. HttpRuntime.Cache.Insert(BuildCacheKey(key), BuildCacheEntry<T>(value));
  56. }
  57.  
  58. //slidingExpiration 时间内无访问则过期
  59. public void Overwrite<T>(String key, T value, TimeSpan slidingExpiration) {
  60. HttpRuntime.Cache.Insert(BuildCacheKey(key), BuildCacheEntry<T>(value), null,
  61. Cache.NoAbsoluteExpiration, slidingExpiration);
  62. }
  63.  
  64. //absoluteExpiration 时过期
  65. public void Overwrite<T>(String key, T value, DateTime absoluteExpiration) {
  66. HttpRuntime.Cache.Insert(BuildCacheKey(key), BuildCacheEntry<T>(value), null,
  67. absoluteExpiration, Cache.NoSlidingExpiration);
  68. }
  69.  
  70. public override void Expire(String key) {
  71. HttpRuntime.Cache.Remove(BuildCacheKey(key));
  72. }
  73.  
  74. internal Boolean Hit(DictionaryEntry entry) {
  75. return (entry.Key is String)
  76. && ((String)entry.Key).StartsWith(BuildCacheKey(String.Empty));
  77. }
  78. }

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();
  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. public T GetOrCreate<T>(String key, Func<T> function, TimeSpan slidingExpiration) {
  23. T value;
  24. if (TryGet<T>(key, out value)) {
  25. return value;
  26. }
  27. value = function();
  28. Overwrite(key, value, slidingExpiration);
  29. return value;
  30. }
  31.  
  32. public T GetOrCreate<T>(String key, Func<T> function, DateTime absoluteExpiration) {
  33. T value;
  34. if (TryGet<T>(key, out value)) {
  35. return value;
  36. }
  37. value = function();
  38. Overwrite(key, value, absoluteExpiration);
  39. return value;
  40. }
  41.  
  42. public override void Overwrite<T>(String key, T value) {
  43. _client.Store(StoreMode.Set, BuildCacheKey(key), value);
  44. }
  45.  
  46. //slidingExpiration 时间内无访问则过期
  47. public void Overwrite<T>(String key, T value, TimeSpan slidingExpiration) {
  48. _client.Store(StoreMode.Set, BuildCacheKey(key), value, slidingExpiration);
  49. }
  50.  
  51. //absoluteExpiration 时过期
  52. public void Overwrite<T>(String key, T value, DateTime absoluteExpiration) {
  53. _client.Store(StoreMode.Set, BuildCacheKey(key), value, absoluteExpiration);
  54. }
  55.  
  56. public override void Expire(String key) {
  57. _client.Remove(BuildCacheKey(key));
  58. }
  59. }

EnyimMemcached 天然支持空缓存项,另外过期时间会因为客户端与服务器时间不严格一致出现测试未通过的情况,它不推荐使用过多的 MemcachedClient 实例,所以此处写成单例形式,另外如何配置等问题,请翻看项目的 Github,本文只使用了最基本的配置,见源代码,更多设置项及解释见 Github

需要注意的是,EnyimMemcached 处理的自定义对象需要使用 [Serializable] 修饰,不然操作无效且不报错,存在产生重大Bug的可能;

最后是工厂类 CacheProviderFactory 的实现,这里从类库项目中排除掉了,即可以是形如 #if DEBUG 类的条件编译,也可以按配置文件来,个人感觉应该在应用中提供统一的入口功能即可。另外 Memcached 的特性本文使用有限,所以未从新接口派生,各位看自己需求扩展既是。

补图:

包含测试用例的源码见 Github , jusfr 原创,转载请注明来自博客园

一步步实现一个基本的缓存模块·续, 添加Memcached调用实现的更多相关文章

  1. Intellij idea 一个窗口打开多模块并添加依赖

    打开多模块 ctrl+alt+shift+s 或者file->project sturcture 选择modules 添加 选择要添加的模块 选择从现有模块添加,不要选择从现在代码创建模块 添加 ...

  2. 哪种缓存效果高?开源一个简单的缓存组件j2cache

    背景 现在的web系统已经越来越多的应用缓存技术,而且缓存技术确实是能实足的增强系统性能的.我在项目中也开始接触一些缓存的需求. 开始简单的就用jvm(java托管内存)来做缓存,这样对于单个应用服务 ...

  3. 【Java EE 学习 78 上】【数据采集系统第十天】【Service使用Spring缓存模块】

    一.需求分析 调查问卷中或许每一个单击动作都会引发大量的数据库访问,特别是在参与调查的过程中,只是单击“上一页”或者“下一页”的按钮就会引发大量的查询,必须对这种问题进行优化才行.使用缓存策略进行查询 ...

  4. .NET 缓存模块设计

    上一篇谈了我对缓存的概念,框架上的理解和看法,这篇承接上篇讲讲我自己的缓存模块设计实践. 基本的缓存模块设计 最基础的缓存模块一定有一个统一的CacheHelper,如下: public interf ...

  5. IOS编程 图片缓存模块设计

    手机客户端为什么会留存下来?而不是被一味的Wap替代掉?因为手机客户端有Wap无可替代的优势,就是自身较强的计算能力. 手机中不可避免的一环:图片缓存,在软件的整个运行过程中显得尤为重要. 先简单说一 ...

  6. [.NET] 一步步打造一个简单的 MVC 网站 - BooksStore(一)

    一步步打造一个简单的 MVC 网站 - BooksStore(一) 本系列的 GitHub地址:https://github.com/liqingwen2015/Wen.BooksStore 简介 主 ...

  7. [.NET] 一步步打造一个简单的 MVC 电商网站 - BooksStore(二)

    一步步打造一个简单的 MVC 电商网站 - BooksStore(二) 本系列的 GitHub地址:https://github.com/liqingwen2015/Wen.BooksStore 前: ...

  8. [.NET] 一步步打造一个简单的 MVC 电商网站 - BooksStore(一)

    一步步打造一个简单的 MVC 电商网站 - BooksStore(一) 本系列的 GitHub地址:https://github.com/liqingwen2015/Wen.BooksStore &l ...

  9. 使用spring EL表达式+自定义切面封装缓存模块

    需求是这样的,业务代码需要使用到缓存功能以减少数据库压力,使用redis来实现,并且需要生成缓存的key由方法的传参拼接而成(貌似也只能这样才能保证同样的select查询可以使用缓存),简单的方式就是 ...

随机推荐

  1. centos7.4 nfs-2.3.2

    http://www.linuxfromscratch.org/blfs/view/svn/basicnet/libtirpc.html 注释:安装环境centos7.4;   安装完软件成后会升级系 ...

  2. [EffectiveC++]item44:将与参数无关的代码抽离templates

  3. 获取索引--------用range()和len()

    a = ['Google', 'Baidu', 'Runoob', 'Taobao', 'QQ'] for i in range(len(a)): print(i+1,a[i])

  4. MyISAM和innoDB对比,覆盖索引简单回顾

    MyISAM Myisam是Mysql的默认存储引擎,当create创建新表时,未指定新表的存储引擎时,默认使用Myisam. 它不支持事务,也不支持外键,尤其是访问速度快,对事务完整性没有要求或者以 ...

  5. Scala学习——Scala By Example——to be continued

    这一篇是来自官网的Scala By Example 即Tutorial后更详细地对Scala怎么用给了示例 该文一开始给了一个快速排序的示例 版本一: def sort(xs: Array[Int]) ...

  6. Java虚拟机16:Java内存模型

    什么是Java内存模型 Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的访问差异,以实现让Java程序在各种平台下都能达到一致 ...

  7. UVa 10213 - How Many Pieces of Land ?(欧拉公式)

    链接: https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&page=show_problem& ...

  8. 【2016蓝桥杯省赛】试题C++ B组试题

    一.    煤球数目 作答:171700 #include <iostream> using namespace std; int main() { ,x=; ;i<=;i++){ ...

  9. bootstrap 多级下拉菜单

    如上效果: 实现代码: 导入js和css: <link rel="stylesheet" href="http://cdn.static.runoob.com/li ...

  10. java的@PostConstruct注解

    javax.annotation 注释类型 PostConstruct @Documented @Retention(value=RUNTIME) @Target(value=METHOD) publ ...