[转载]简单利用Memcached进行缓存层设计 – Jeff Wong – 博客园.
正在考虑web应用缓存层的设计,参考了不少资料,估计还是需要用到相对成熟应用广泛的分布式缓存Memcached。在.net平台上早就有相对成熟的Memcached客户端产品,如BeITMemcached和EnyimMemcached,业余时间看了一下源码,自己分析并调用一下并不困难。这里简单介绍一下利用Memcached的一个简单的缓存层设计,示例代码基于EnyimMemcached,下面以贴代码为主。
一、公共缓存接口
分析ASP.NET web caching的缓存类,我们大致可以抽象出如下几个接口方法:
namespace DotNet.Common.EnyimCache { /// /// memcached公共缓存调用方法接口(读) /// public interface ICacheReaderService { /// /// 返回指定key的对象 /// /// <span> </span> /// object Get(string key); /// /// 返回指定key的对象 /// /// /// <span> </span> /// T Get(string key); /// /// 是否存在 /// /// <span> </span> /// bool isExists(string key); } /// /// memcached公共缓存调用方法接口(写) /// public interface ICacheWriterService { /// /// 缓存有效间隔时间 (以分钟为单位) /// int TimeOut { set; get; } /// /// 添加指定key的对象 /// /// <span> </span> /// <span> </span> void Add(string key, object obj); /// /// 添加指定key的对象 /// /// /// <span> </span> /// <span> </span> void Add(string key, T obj); /// /// 移除指定key的对象 /// /// <span> </span> bool Remove(string key); /// /// 修改指定key的对象 /// /// <span> </span> /// bool Modify(string key, object destObj); /// /// 清空缓存 /// /// bool Release(); } }
看命名就知道,增删改查是也。根据个人使用缓存的经验,修改操作通常是不需要的,如果确实需要修改缓存数据,直接删除然后添加就是改了。
还有,你可能会问,这里为什么要定义两个接口?原因主要是考虑到读操作(查询)是经常使用的,而写操作(增删改)相对较少,所以也把它们设计成读写分离的方式。
二、缓存服务实现
这里就需要调用Memcached客户端封装好的调用方法,实现增删改查等方法。
using System; namespace DotNet.Common.EnyimCache { using Enyim.Caching.Memcached; public class CacheReaderService : BaseService, ICacheReaderService { public int TimeOut { get; set; } public CacheReaderService() { } public object Get(string key) { object obj = null; Client.TryGet(key, out obj); return obj; } public T Get(string key) { object obj = Get(key); T result = default(T); if (obj != null) { result = (T)obj; } return result; } public bool isExists(string key) { object obj = Get(key); return (obj == null) ? false : true; } } public class CacheWriterService : BaseService, ICacheWriterService { public int TimeOut { get; set; } public CacheWriterService() { } public CacheWriterService(int timeOut) { this.TimeOut = timeOut; } public void Add(string key, object obj) { if (TimeOut > 0) { Client.Store(StoreMode.Add, key, obj, DateTime.Now.AddMinutes(TimeOut)); } else { Client.Store(StoreMode.Add, key, obj); } } public void Add(string key, T obj) { if (TimeOut > 0) { Client.Store(StoreMode.Add, key, obj, DateTime.Now.AddMinutes(TimeOut)); } else { Client.Store(StoreMode.Add, key, obj); } } public bool Remove(string key) { return Client.Remove(key); } public bool Modify(string key, object destObj) { return Client.Store(StoreMode.Set, key, destObj); } /// /// 清空缓存 TO DO /// /// public bool Release() { throw new NotImplementedException(); } } }
基类里初始化一个MemcachedClient示例Client,这个Client的方法里封装了较多的函数。查看源码可以知道,它们本质上都是 向Memcached服务端发送相关指令(run command),然后解析返回的二进制数据,如果您熟悉memcached所使用的协议,理解起来应该会相当简单。本文示例只使用了客户端提供的几个方 法。
同时要注意,在实现具体缓存服务的时候,CacheWriterService有两个构造函数,其中带参数的是为缓存显式指定过期时间。这个参数在实际应用中通常需要配置,显然是比较灵活一些的。
备注:在接口中有一个函数Release,本来的目标是清空所有的缓存数据,但是客户端没有直接提供对应的函数,如果您有好的方法,请不吝赐教。
三、简单的读写测试
贴一下字符串、时间、单个类和集合的增删改查示例代码:
ICacheWriterService writer = CacheBuilder.GetWriterService();//writer 使用memcached默认过期时间 ICacheReaderService reader = CacheBuilder.GetReaderService();//reader #region 字符串 string strKey = "hello"; bool isOK = writer.Remove(strKey); //移除 Console.WriteLine("Removed key {0}:{1}", strKey, isOK); writer.Add(strKey, "hello world"); //添加 Console.WriteLine("Add key {0}, value:hello world", strKey); bool isExists = reader.isExists(strKey);//是否存在 Console.WriteLine("Key {0} exists:{1}", strKey, isExists); string result = reader.Get(strKey) as string;//查询 Console.WriteLine("Get key {0}:{1}", strKey, result); bool isModify = writer.Modify(strKey, "Hello Memcached!");//修改 Console.WriteLine("Modify key {0}, value:Hello Memcached. The result is:{1}", strKey, isModify); result = reader.Get(strKey); Console.WriteLine("Generic get key {0}:{1}", strKey, result); isOK = writer.Remove(strKey); Console.WriteLine("Removed key {0}:{1}", strKey, isOK); isExists = reader.isExists(strKey); Console.WriteLine("Key {0} exists:{1}", strKey, isExists); result = reader.Get(strKey) as string; Console.WriteLine("Get key {0}:{1}", strKey, result); result = reader.Get(strKey); Console.WriteLine("Generic get key {0}:{1}", strKey, result); Console.WriteLine(); Console.WriteLine("==========================================="); Console.Read(); #endregion #region 时间 DateTime dtNow = DateTime.Now; strKey = "datetime"; isOK = writer.Remove(strKey); //移除 Console.WriteLine("Removed key {0}:{1}", strKey, isOK); writer.Add(strKey, dtNow); //添加 Console.WriteLine("Add key {0}, value:{1}", strKey, dtNow); isExists = reader.isExists(strKey);//是否存在 Console.WriteLine("Key {0} exists:{1}", strKey, isExists); DateTime dt = (DateTime)reader.Get(strKey);//查询 Console.WriteLine("Get key {0}:{1}", strKey, dt); dt = reader.Get(strKey); Console.WriteLine("Generic get key {0}:{1}", strKey, dt); isOK = writer.Remove(strKey); Console.WriteLine("Removed key {0}:{1}", strKey, isOK); isExists = reader.isExists(strKey); Console.WriteLine("Key {0} exists:{1}", strKey, isExists); Console.WriteLine("Get key {0}:{1}", strKey, reader.Get(strKey)); Console.WriteLine("Generic get key {0}:{1}", strKey, reader.Get(strKey));//default(datetime) Console.WriteLine(); Console.WriteLine("==========================================="); Console.Read(); #endregion #region 类 dtNow = DateTime.Now; Province province = new Province(13579, "江苏", dtNow, dtNow); strKey = string.Format("{0}_{1}", province.GetType().Name, province.Id);//省 isOK = writer.Remove(strKey); //移除 Console.WriteLine("Removed key {0}:{1}", strKey, isOK); writer.Add(strKey, province); //添加 Console.WriteLine("Add key {0}, value:{1}", strKey, dtNow); isExists = reader.isExists(strKey);//是否存在 Console.WriteLine("Key {0} exists:{1}", strKey, isExists); Province queryProvince = (Province)reader.Get(strKey);//查询 Console.WriteLine("Get key {0}:{1}", strKey, queryProvince.ProvinceName); queryProvince = reader.Get (strKey); Console.WriteLine("Generic get key {0}:{1}", strKey, queryProvince.ProvinceName); isOK = writer.Remove(strKey); Console.WriteLine("Removed key {0}:{1}", strKey, isOK); isExists = reader.isExists(strKey); Console.WriteLine("Key {0} exists:{1}", strKey, isExists); Console.WriteLine("Get key {0}:{1}", strKey, reader.Get(strKey)); Console.WriteLine("Generic get key {0}:{1}", strKey, reader.Get (strKey)); Console.WriteLine(); Console.WriteLine("==========================================="); Console.Read(); #endregion #region 集合(列表) dtNow = DateTime.Now; IList listCities = new List(); City city = new City(135, province.Id, "南京", "210000", dtNow, dtNow); listCities.Add(city); city = new City(246, province.Id, "苏州", "215000", dtNow, dtNow); listCities.Add(city); strKey = string.Format("List_{0}_{1}_{2}", province.GetType().Name, province.Id, city.GetType().Name);//省份对应城市 isOK = writer.Remove(strKey); //移除 Console.WriteLine("Removed key {0}:{1}", strKey, isOK); writer.Add(strKey, listCities); //添加 Console.WriteLine("Add key {0}, value:", strKey); foreach (var item in listCities) { Console.WriteLine("CityId:{0} CityName:{1}", item.Id, item.CityName); } isExists = reader.isExists(strKey);//是否存在 Console.WriteLine("Key {0} exists:{1}", strKey, isExists); IList queryCities = reader.Get(strKey) as IList;//查询 Console.WriteLine("Get key {0}:", strKey); foreach (var item in queryCities) { Console.WriteLine("CityId:{0} CityName:{1}", item.Id, item.CityName); } queryCities = reader.Get>(strKey); Console.WriteLine("Generic get key {0}:", strKey); foreach (var item in queryCities) { Console.WriteLine("CityId:{0} CityName:{1}", item.Id, item.CityName); } isOK = writer.Remove(strKey); Console.WriteLine("Removed key {0}:{1}", strKey, isOK); isExists = reader.isExists(strKey); Console.WriteLine("Key {0} exists:{1}", strKey, isExists); Console.WriteLine("Get key {0}:{1}", strKey, reader.Get(strKey)); Console.WriteLine("Generic get key {0}:{1}", strKey, reader.Get>(strKey)); Console.WriteLine(); Console.WriteLine("==========================================="); Console.Read(); #endregion #region 集合(字典) dtNow = DateTime.Now; IDictionary dictCities = new Dictionary(); city = new City(123, province.Id, "镇江", "212000", dtNow, dtNow); dictCities.Add(city.Id, city); city = new City(321, province.Id, "扬州", "225000", dtNow, dtNow); dictCities.Add(city.Id, city); strKey = string.Format("Dictionary_{0}_{1}_{2}", province.GetType().Name, province.Id, city.GetType().Name);//省份对应城市 isOK = writer.Remove(strKey); //移除 Console.WriteLine("Removed key {0}:{1}", strKey, isOK); writer.Add(strKey, dictCities); //添加 Console.WriteLine("Add key {0}, value:", strKey); foreach (var item in dictCities) { Console.WriteLine("CityId:{0} CityName:{1}", item.Key, item.Value.CityName); } isExists = reader.isExists(strKey);//是否存在 Console.WriteLine("Key {0} exists:{1}", strKey, isExists); IDictionary queryDictCities = reader.Get(strKey) as IDictionary;//查询 Console.WriteLine("Get key {0}:", strKey); foreach (var item in queryDictCities) { Console.WriteLine("CityId:{0} CityName:{1}", item.Key, item.Value.CityName); } queryDictCities = reader.Get>(strKey); Console.WriteLine("Generic get key {0}:", strKey); foreach (var item in queryDictCities) { Console.WriteLine("CityId:{0} CityName:{1}", item.Key, item.Value.CityName); } isOK = writer.Remove(strKey); Console.WriteLine("Removed key {0}:{1}", strKey, isOK); isExists = reader.isExists(strKey); Console.WriteLine("Key {0} exists:{1}", strKey, isExists); Console.WriteLine("Get key {0}:{1}", strKey, reader.Get(strKey)); Console.WriteLine("Generic get key {0}:{1}", strKey, reader.Get>(strKey)); Console.WriteLine(); Console.WriteLine("==========================================="); Console.Read(); #endregion
这里就不贴全部代码了,文章最后有示例可以下载。
在我的简单测试中,对常见基础数据类型如(字符串、数组、数字和时间)、集合(列表和字典)都有良好的表现,对datatable和dataset同样表现不俗,但是不太建议直接缓存这两种重粒度的类型。
在显式指定过期时间的示例中,指定过期时间是一分钟,但是memcached实际过期时间有时候好像会多于一分钟,估计是系统内部的延迟。
在本地计算机上进行10万次循环添加缓存的过程中,发现系统内存果然增加的非常厉害。然后查询性能并没有显著下降,也许和我的单机测试环境有关,所以我认为测试结果并没有说服力,要知道,memcached的优势是它的分布式缓存实现。
有人发现如何保证缓存系统的键唯一也非常令人头疼。同样的缓存框架,不同项目不同开发者如何保证自己程序添加的缓存键唯一呢?有一种简单方法就是通 过拼接字符串成为有意义的主键,比如按照项目名、命名空间、类名、数据库中的主键组合构成主键等等。当然了,在查询的时候也要自己封装特定格式的字符串主 键。个人感觉确实是一个行之有效的方法。
demo下载:SimpleCacheApp