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 方法,我们可以使用抽象类,将前者写成具体实现,将后两者写成抽象方法,由具体子类去实现。

     public interface ICacheProvider {
Boolean TryGet<T>(String key, out T entry);
T GetOrCreate<T>(String key, Func<T> function);
T GetOrCreate<T>(String key, Func<String, T> factory);
void Overwrite<T>(String key, T entry);
void Expire(String key);
} public abstract class CacheProvider : ICacheProvider {
protected virtual String BuildCacheKey(String key) {
return key;
} protected abstract Boolean InnerTryGet(String key, out Object entry); public virtual Boolean TryGet<T>(String key, out T entry) {
String cacheKey = BuildCacheKey(key);
Object cacheEntry;
Boolean exist = InnerTryGet(cacheKey, out cacheEntry);
if (exist) {
if (cacheEntry != null) {
if (!(cacheEntry is T)) {
throw new InvalidOperationException(String.Format("缓存项`[{0}]`类型错误, {1} or {2} ?",
key, cacheEntry.GetType().FullName, typeof(T).FullName));
}
entry = (T)cacheEntry;
}
else {
entry = (T)((Object)null);
}
}
else {
entry = default(T);
}
return exist;
} public virtual T GetOrCreate<T>(String key, Func<T> function) {
T entry;
if (TryGet(key, out entry)) {
return entry;
}
entry = function();
Overwrite(key, entry);
return entry;
} public virtual T GetOrCreate<T>(String key, Func<String, T> factory) {
T entry;
if (TryGet(key, out entry)) {
return entry;
}
entry = factory(key);
Overwrite(key, entry);
return entry;
} public abstract void Overwrite<T>(String key, T value); public abstract void Expire(String key);
}

抽象类 CacheProvider 的 InnerTryGet、Overwrite、Expire 是需要实现类来完成的,GetOrCreate 调用它们来完成核心逻辑;于是 HttpContextCacheProvider 的实现,逻辑在父类实现后,看起来非常简洁了:

     public class HttpContextCacheProvider : CacheProvider, ICacheProvider {
private const String _prefix = "HttpContextCacheProvider_";
protected override String BuildCacheKey(String key) {
return String.Concat(_prefix, key);
} protected override Boolean InnerTryGet(String key, out Object entry) {
Boolean exist = false;
entry = null;
if (HttpContext.Current.Items.Contains(key)) {
exist = true;
entry = HttpContext.Current.Items[key];
}
return exist;
} public override void Overwrite<T>(String key, T entry) {
HttpContext.Current.Items[BuildCacheKey(key)] = entry;
} public override void Expire(String key) {
HttpContext.Current.Items.Remove(BuildCacheKey(key));
}
}

这里不准备为基于 HttpContext 的缓存提供太多特性,但基于 HttpRuntime.Cache 的缓存就需要像过期之类的功能,在实现之前先考虑问题2

首先,既然用户没有必要甚至不知道分区存在,我们直接实现支持分区特性的子类好了;然后,计数与过期功能 HttpRuntime.Cache 支持但 Memcached 不,所以这部分功能需要从 IHttpRuntimeCacheProvider 中拆分出来,没错,扩展方法!于是拆分如下:

     public interface IRegion {
String Region { get; }
} public interface IHttpRuntimeCacheProvider : ICacheProvider {
T GetOrCreate<T>(String key, Func<T> function, TimeSpan slidingExpiration);
T GetOrCreate<T>(String key, Func<T> function, DateTime absoluteExpiration);
void Overwrite<T>(String key, T value, TimeSpan slidingExpiration);
void Overwrite<T>(String key, T value, DateTime absoluteExpiration);
}

其中IHttpRuntimeCacheProvider接口定义了带有过期参数的缓存操作方法,我们需要实现抽象方法与额外接口如下:

 public class HttpRuntimeCacheProvider : CacheProvider, IHttpRuntimeCacheProvider, IRegion {
private static readonly Object _nullEntry = new Object();
private String _prefix = "HttpRuntimeCacheProvider_"; public virtual String Region { get; private set; } public HttpRuntimeCacheProvider() {
} public HttpRuntimeCacheProvider(String region) {
Region = region;
} protected override bool InnerTryGet(String key, out object entry) {
entry = HttpRuntime.Cache.Get(key);
return entry != null;
} protected override String BuildCacheKey(String key) {
//Region 为空将被当作 String.Empty 处理
return Region == null
? String.Concat(_prefix, key)
: String.Concat(_prefix, Region, key);
} private Object BuildCacheEntry<T>(T value) {
Object entry = value;
if (value == null) {
entry = _nullEntry;
}
return entry;
} public T GetOrCreate<T>(String key, Func<T> function, TimeSpan slidingExpiration) {
T value;
if (TryGet<T>(key, out value)) {
return value;
}
value = function();
Overwrite(key, value, slidingExpiration);
return value;
} public T GetOrCreate<T>(String key, Func<T> function, DateTime absoluteExpiration) {
T value;
if (TryGet<T>(key, out value)) {
return value;
}
value = function();
Overwrite(key, value, absoluteExpiration);
return value;
} public override void Overwrite<T>(String key, T value) {
HttpRuntime.Cache.Insert(BuildCacheKey(key), BuildCacheEntry<T>(value));
} //slidingExpiration 时间内无访问则过期
public void Overwrite<T>(String key, T value, TimeSpan slidingExpiration) {
HttpRuntime.Cache.Insert(BuildCacheKey(key), BuildCacheEntry<T>(value), null,
Cache.NoAbsoluteExpiration, slidingExpiration);
} //absoluteExpiration 时过期
public void Overwrite<T>(String key, T value, DateTime absoluteExpiration) {
HttpRuntime.Cache.Insert(BuildCacheKey(key), BuildCacheEntry<T>(value), null,
absoluteExpiration, Cache.NoSlidingExpiration);
} public override void Expire(String key) {
HttpRuntime.Cache.Remove(BuildCacheKey(key));
} internal Boolean Hit(DictionaryEntry entry) {
return (entry.Key is String)
&& ((String)entry.Key).StartsWith(BuildCacheKey(String.Empty));
}
}

HttpRuntimeCacheProvider 暴露了一个 internal 修饰的方法,提供给扩展方法调用:

     public static class HttpRuntimeCacheProviderExtensions {

         public static void ExpireAll(this HttpRuntimeCacheProvider cacheProvider) {
var entries = HttpRuntime.Cache.OfType<DictionaryEntry>()
.Where(cacheProvider.Hit);
foreach (var entry in entries) {
HttpRuntime.Cache.Remove((String)entry.Key);
}
} public static Int32 Count(this HttpRuntimeCacheProvider cacheProvider) {
return HttpRuntime.Cache.OfType<DictionaryEntry>()
.Where(cacheProvider.Hit).Count();
} public static String Dump(this HttpRuntimeCacheProvider cacheProvider) {
var builder = new StringBuilder();
builder.AppendLine("--------------------HttpRuntimeCacheProvider.Dump--------------------------");
builder.AppendFormat("EffectivePercentagePhysicalMemoryLimit: {0}\r\n", HttpRuntime.Cache.EffectivePercentagePhysicalMemoryLimit);
builder.AppendFormat("EffectivePrivateBytesLimit: {0}\r\n", HttpRuntime.Cache.EffectivePrivateBytesLimit);
builder.AppendFormat("Count: {0}\r\n", HttpRuntime.Cache.Count);
builder.AppendLine();
var entries = HttpRuntime.Cache.OfType<DictionaryEntry>().Where(cacheProvider.Hit).OrderBy(de => de.Key);
foreach (var entry in entries) {
builder.AppendFormat("{0}\r\n {1}\r\n", entry.Key, entry.Value.GetType().FullName);
}
builder.AppendLine("--------------------HttpRuntimeCacheProvider.Dump--------------------------");
Debug.WriteLine(builder.ToString());
return builder.ToString();
}
}

考虑到计数、全部过期等功能并不常用,所以这里基本实现功能,并未周全地考虑并发、效率问题;至此功能拆分完成,我们转入 Memcached 实现

Memcached 客户端有相当多的C#实现,这里我选择了 EnyimMemcached,最新版本为2.12,见 https://github.com/enyim/EnyimMemcached 。与 HttpRuntimeCacheProvider 非常类似,从 CacheProvider 继承,实现 IHttpRuntimeCacheProvider, IRegion 接口,完成必要的逻辑即可。

     public class MemcachedCacheProvider : CacheProvider, IHttpRuntimeCacheProvider, IRegion {
private static readonly MemcachedClient _client = new MemcachedClient("enyim.com/memcached"); public String Region { get; private set; } public MemcachedCacheProvider()
: this(String.Empty) {
} public MemcachedCacheProvider(String region) {
Region = region;
} protected override String BuildCacheKey(String key) {
return Region == null ? key : String.Concat(Region, "_", key);
} protected override bool InnerTryGet(string key, out object entry) {
return _client.TryGet(key, out entry);
} public T GetOrCreate<T>(String key, Func<T> function, TimeSpan slidingExpiration) {
T value;
if (TryGet<T>(key, out value)) {
return value;
}
value = function();
Overwrite(key, value, slidingExpiration);
return value;
} public T GetOrCreate<T>(String key, Func<T> function, DateTime absoluteExpiration) {
T value;
if (TryGet<T>(key, out value)) {
return value;
}
value = function();
Overwrite(key, value, absoluteExpiration);
return value;
} public override void Overwrite<T>(String key, T value) {
_client.Store(StoreMode.Set, BuildCacheKey(key), value);
} //slidingExpiration 时间内无访问则过期
public void Overwrite<T>(String key, T value, TimeSpan slidingExpiration) {
_client.Store(StoreMode.Set, BuildCacheKey(key), value, slidingExpiration);
} //absoluteExpiration 时过期
public void Overwrite<T>(String key, T value, DateTime absoluteExpiration) {
_client.Store(StoreMode.Set, BuildCacheKey(key), value, absoluteExpiration);
} public override void Expire(String key) {
_client.Remove(BuildCacheKey(key));
}
}

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. HTML-head头部浅析

    HTML结构 在sublime或HBuildr新建HTML文件,输入html:5,按下tab键后,自动生成的代码大致如下: <!DOCTYPE html> <html lang=&q ...

  2. 改变文件上传input file类型的外观

    当我们使用文件上传功能时,<input type="file">,但是外观有点不符合口味,如何解决这个问题? <input type="file&quo ...

  3. 关于HTML Button点击自动刷新页面的问题解决

    原因 button,input type=button按钮在IE和w3c,firefox浏览器区别: 1.当在IE浏览器下面时,button标签按钮,input标签type属性为button的按钮是一 ...

  4. node.js 连接 sql server 包括低版本的sqlserver 2000

    利用tedious连接,github地址:https://github.com/tediousjs/tedious 废话不多时直接上代码. connection.js var Connection = ...

  5. IFsvnadmin svn界面管理工具

    安装部署if.svnadmin 工具 前提是安装好svn服务器及apache+php服务器. yum -y install subversion mod_dav_svn 安装完建立一个目录用来作为sv ...

  6. VC++获取当前路径及程序名的实现代码

    VC上或取当前路径有多种方法,最常用的是使用 GetCurrentDirectory和GetModuleFileName函数,个中都有诸多注意事项,特别总结一下 一.获取当前运行目录的绝对路径 1.使 ...

  7. Oracle 数据库纯dos代码操作

    1. 安装成功后进入DOS界面操作 在进行以下操作时,需启动Oracle服务. A.进入sql界面:开始--运行--cmd:输入sqlplus 回车 提示输入正确的用户名和密码 B.开始—>所有 ...

  8. Uva442

    https://vjudge.net/problem/UVA-442 思路: 1)当遇到左括号将字母进栈,遇到右括号将字母出栈. 2) isalpha() 判断一个字符是否是字母 int isalph ...

  9. ZOJ3202-Second-price Auction(根据输入输出判断是队列还是栈)

    A Stack or A Queue? Time Limit:1000MS     Memory Limit:32768KB     64bit IO Format:%lld & %llu S ...

  10. Lambda 表达式的示例

    本文中的过程演示如何使用 lambda 表达式. 有关 lambda 表达式的概述,请参见 C++ 中的 Lambda 表达式. 有关 lambda 表达式结构的更多信息,请参见 Lambda 表达式 ...