一步步实现一个基本的缓存模块·续, 添加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 方法,我们可以使用抽象类,将前者写成具体实现,将后两者写成抽象方法,由具体子类去实现。
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调用实现的更多相关文章
- Intellij idea 一个窗口打开多模块并添加依赖
打开多模块 ctrl+alt+shift+s 或者file->project sturcture 选择modules 添加 选择要添加的模块 选择从现有模块添加,不要选择从现在代码创建模块 添加 ...
- 哪种缓存效果高?开源一个简单的缓存组件j2cache
背景 现在的web系统已经越来越多的应用缓存技术,而且缓存技术确实是能实足的增强系统性能的.我在项目中也开始接触一些缓存的需求. 开始简单的就用jvm(java托管内存)来做缓存,这样对于单个应用服务 ...
- 【Java EE 学习 78 上】【数据采集系统第十天】【Service使用Spring缓存模块】
一.需求分析 调查问卷中或许每一个单击动作都会引发大量的数据库访问,特别是在参与调查的过程中,只是单击“上一页”或者“下一页”的按钮就会引发大量的查询,必须对这种问题进行优化才行.使用缓存策略进行查询 ...
- .NET 缓存模块设计
上一篇谈了我对缓存的概念,框架上的理解和看法,这篇承接上篇讲讲我自己的缓存模块设计实践. 基本的缓存模块设计 最基础的缓存模块一定有一个统一的CacheHelper,如下: public interf ...
- IOS编程 图片缓存模块设计
手机客户端为什么会留存下来?而不是被一味的Wap替代掉?因为手机客户端有Wap无可替代的优势,就是自身较强的计算能力. 手机中不可避免的一环:图片缓存,在软件的整个运行过程中显得尤为重要. 先简单说一 ...
- [.NET] 一步步打造一个简单的 MVC 网站 - BooksStore(一)
一步步打造一个简单的 MVC 网站 - BooksStore(一) 本系列的 GitHub地址:https://github.com/liqingwen2015/Wen.BooksStore 简介 主 ...
- [.NET] 一步步打造一个简单的 MVC 电商网站 - BooksStore(二)
一步步打造一个简单的 MVC 电商网站 - BooksStore(二) 本系列的 GitHub地址:https://github.com/liqingwen2015/Wen.BooksStore 前: ...
- [.NET] 一步步打造一个简单的 MVC 电商网站 - BooksStore(一)
一步步打造一个简单的 MVC 电商网站 - BooksStore(一) 本系列的 GitHub地址:https://github.com/liqingwen2015/Wen.BooksStore &l ...
- 使用spring EL表达式+自定义切面封装缓存模块
需求是这样的,业务代码需要使用到缓存功能以减少数据库压力,使用redis来实现,并且需要生成缓存的key由方法的传参拼接而成(貌似也只能这样才能保证同样的select查询可以使用缓存),简单的方式就是 ...
随机推荐
- TreeSet的自然排序(自定义对象 compareTo方法)
>要实现自然排序,对象集合必须实现Comparable接口,并重写compareTo()方法 >一般需求中描述的是"主要条件",如:按姓名长度排序. 需注意次要条件 ...
- jQuery Ajax url使用方式
jQuery Ajax的使用场景: 页面需要通过后台逻辑,但只需要局部刷新以显示新的内容. jQuery Ajax url使用方式1.servlet方式: 需要在struts.xml中写一个actio ...
- Cloudstack
1.cloudstack介绍 一个开源具有高可用性及扩展性的云计算平台,Cloudstack是一个开源的云操作系统: cloudstack支持管理大部分主流的hypervisors,如:VMware, ...
- SOAR平台初探(一)
1.前言 Security Orchestration, Automation and Response(SOAR)安全编排和自动化响应,是Gartner2017年提出的新概念.Gartner预计到2 ...
- Java Classloader机制解析
做Java开发,对于ClassLoader的机制是必须要熟悉的基础知识,本文针对Java ClassLoader的机制做一个简要的总结.因为不同的JVM的实现不同,本文所描述的内容均只限于Hotspo ...
- Perl之my与local
版权声明:本文为博主原创文章,未经博主同意不得转载. https://blog.csdn.net/sunshoupo211/article/details/31745909 在函数定义中,使用m ...
- redis key/value 出现\xAC\xED\x00\x05t\x00\x05
1.问题现象: 最近使用spring-data-redis 和jedis 操作redis时发现存储在redis中的key不是程序中设置的string值,前面还多出了许多类似\xac\xed\x00\x ...
- ASP.NET web api 跨域请求
1.学习文章:AJAX 跨域请求 - JSONP获取JSON数据 1.asp.net代码 参考文章:http://www.sxt.cn/info-2790-u-756.html (1).增加CorsH ...
- luogu P3369 【模板】普通平衡树(splay)
嘟嘟嘟 突然觉得splay挺有意思,唯一不足的是这几天是一天一道,debug到崩溃. 做了几道平衡树基础题后,对这题有莫名的自信,还算愉快的敲完了代码后,发现样例都过不去,然后就陷入了无限的debug ...
- Google 地图切片URL地址解析
一.Google地图切片的投影方式及瓦片索引机制 1.地图投影 Google地图采用的是Web墨卡托投影(如下图),为了方便忽略了两极变形较大的地区,把世界地图做成了一个边长等于赤道周长的正方形(赤道 ...