使用webapi做rest的服务接口时,有些读取数据表的操作,数据本身变化不频繁,但是访问量却不小,比如频道分类,地市选择信息等等等等。这时,必然想到使用缓存。
在普通controller下,由于controller实现了一堆接口,其中包括了很多的filter,所以,可以轻松的实现缓存,如果只需要页面级别缓存,则大可以使用之前提到的OutputCacheAttribute,轻松搞定。
但是,在webapi下,一切都简化了,apicontroller只实现了IHttpController, IDisposable两个接口,其他什么都没留下。所以,不要指望重写OnActionExecuting、OnActionExecuted之类的事件来向action(webapi下是不是还能叫做action不清楚)执行前后添加代码了。
怎么办呢。考虑写个页面级别的缓存。由于没法默认在方法前后添加代码,我们将方法封装成匿名方法,用一个专门的调用类来调用,并作出调用前判断和调用后缓存等处理。看看效果:
首先定义一个静态类,其中的静态属性用于存储缓存数据:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace XXX.Models
{
public static class CacheContainer
{
private static Hashtable Data = new Hashtable();
public static void SetData(string key, object value)
{
if (Data[key] != null)
{
if (value == null)
{
Data.Remove(key);
}
else
{
Data[key] = value;
}
}
else
{
Data.Add(key, value);
}
}
public static T GetData<T>(string key)
{
return (T)Data[key];
}
}
public class DataCacheUnit
{
public object CacheData { get; set; }
public DateTime LastCache { get; set; }
}
}
就是一个简单的全局数据存取。用哈希表是便于通过唯一标示来快速找到缓存的数据。这里是使用method级别的标示符,并且不带参数。如果需要根据参数来缓存数据,请在计算唯一标示时加上参数。
另外定义了一种用于存储的数据类型,也就是哈希表中的值,包含上次缓存时间和缓存内容。
然后,我们定义一个用于method的attribute,用于标识需要缓存的action(姑且认为还是action吧)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http.Controllers;
using System.Web.Mvc;
namespace XXX.Filter
{
[AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class DataCacheAttribute : Attribute
{
public int CacheSeconds { get; set; }
public DataCacheAttribute()
{
CacheSeconds = 60;
}
public DataCacheAttribute(int CacheSeconds)
{
this.CacheSeconds = CacheSeconds;
}
}
}
这个attribute非常非常简单,只有一个属性,就是缓存时间。
接下来是重点了,定义一个调用类,来代理我们的action(method)调用实现,并在调用之前和之后做些事情:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using XXX.Models;
namespace XXX.Filter
{
/// <summary>
/// 调用操作的工具类
/// </summary>
public static class ActionInvoker
{
public static T Invoke<T>(object target,string actionName, Func<T> method)
{
var targetType = target.GetType();
var targetInfo = targetType.GetMethod(actionName);
if (targetInfo.IsDefined(typeof(DataCacheAttribute), true))
{
var attr = targetInfo
.GetCustomAttributes(typeof(DataCacheAttribute), true)
.OfType<DataCacheAttribute>()
.FirstOrDefault();
var key = targetType.Name + "-" + actionName;
DataCacheUnit data = CacheContainer.GetData<DataCacheUnit>(key);
if (data != null && DateTime.Now < data.LastCache.AddSeconds(attr.CacheSeconds))
{
return (T)data.CacheData;
}
else
{
var rsl = method.Invoke();
CacheContainer.SetData(key, new DataCacheUnit
{
CacheData = rsl,
LastCache = DateTime.Now
});
return rsl;
}
}
else
{
return method.Invoke();
}
}
}
}
这次,没写注释,含义是,首先判断该method(通过target和acitonName传递,target为class的对象,actionName为这个class的method名称)是不是包含DataCache的特性,如果不包含,直接返回method.Invode()。否则,进行判断,如果存在缓存数据且没有过期,则返回缓存的数据,否则,执行 method.Invode(),并将结果缓存下来,然后返回。
其中,使用
targetType.Name + "-" + actionName;
作为唯一标示,并不包含参数。
这个结构搭建好之后,使用起来不是很复杂:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Mvc;
using XXX.Models.Results;
using XXX.Models;
using XXX.Filter;
namespace XXX.Controllers
{
public class UnitController : ApiController
{
[DataCache]
public IEnumerable<UnitResultItem> Get()
{
return ActionInvoker.Invoke(this, "Get", () =>
{
List<UnitResultItem> rsl = new List<UnitResultItem>();
using (ProtectRightsEntities db = new ProtectRightsEntities())
{
var units = db.XXXunit;
foreach (var unit in units)
{
UnitResultItem item = new UnitResultItem
{
id = unit.id,
logo = unit.logo,
name = unit.name
};
rsl.Add(item);
}
}
return rsl;
});
}
}
}
这里是一个普通的apicontroller的get方法,无参数(需要参数的话,自己修改代码。但是不建议使用带参数的缓存,这样参数必须限制在有限的几个内,否则换存量会很大)。 get方式返回一个UnitResultItem类型的实体列表。这里,将之前写的代码完全封装成一个匿名函数,(()=>{}部分),然后,将此匿名函数用ActionInvoker.Invoke代为调用,传参时,第一个传递this,第二个传递当前action名(method),第三个就是封装起来的匿名函数。在匿名函数中,一切原样返回,而整个api的action变成了一句return语句,return ActionInvoker.Invoke(this,"Get",()=>{ /*处理过程*/ return rsl;});
试用下,效果不错,默认是缓存1分钟,改变缓存时间,只需要在[DataCache]修饰的时候,加上参数[DataCache(120)]
昨天写到这里就完了。
今天继续,发现这货还是有很多问题的:
1是确实不支持参数,那么对于可以翻页的接口,明显不能使用了。
2是多线操作静态类,没有做任何互斥,可能产生数据访问冲突。
3是没有对和缓存数量的上限限制,有可能造成大量内存占用。
4是没有对长期不用的缓存数据进行及时清理,可能产生永久性的垃圾数据。
基于以上几点,做了一些修改,如下:
首先是用于存储的存储体:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Web;
namespace XXX.Models
{
public static class CacheContainer
{
private static Hashtable Data = new Hashtable();
private static Mutex DataLock = new Mutex();
private const int MaxCacheCount = 3000;
public static bool SetData(string key, object value, int seconds)
{
bool successLock = DataLock.WaitOne(30000);
if (!successLock)
{
return false;
}
if (Data[key] != null)
{
if (value == null)
{
Data.Remove(key);
}
else
{
Data[key] = new DataCacheUnit
{
CacheData = value,
ExpTime = DateTime.Now.AddSeconds(seconds)
};
}
}
else
{
if (Data.Count >= MaxCacheCount)
{
DataLock.ReleaseMutex();
return false;
}
Data.Add(key, new DataCacheUnit
{
CacheData = value,
ExpTime = DateTime.Now.AddSeconds(seconds)
});
}
DataLock.ReleaseMutex();
return true;
}
public static T GetData<T>(string key)
{
if (Data[key] == null)
{
return default(T);
}
if (DateTime.Now > ((DataCacheUnit)Data[key]).ExpTime)
{
return default(T);
}
else
{
return (T)((DataCacheUnit)Data[key]).CacheData;
}
}
public static bool ClearUp()
{
bool successLock = DataLock.WaitOne(30000);
if (!successLock)
{
return false;
}
DateTime now = DateTime.Now;
List<string> delKeyList = new List<string>();
foreach (string key in Data.Keys)
{
if (now > ((DataCacheUnit)Data[key]).ExpTime)
{
delKeyList.Add(key);
}
}
foreach (string key in delKeyList)
{
Data.Remove(key);
}
DataLock.ReleaseMutex();
return true;
}
private class DataCacheUnit
{
public object CacheData { get; set; }
public DateTime ExpTime { get; set; }
}
}
}
这里首先增加了一个互斥量,用于线程间互斥。由于是hash表,读取无需互斥,写操作需要互斥,这里在清理和设置缓存时加了锁,如果锁在超时时间内未获取,则直接返回,不进行缓存。其次增加的是缓存数量上限,在新增操作时如果已达上限,则放弃缓存,返回失败,当然之前要先释放信号量。最后是增加了定时调用的清理操作ClearUp() ,清理过期的数据缓存,这个需要又全局定时器定时执行,在Global.asax中实现(见下面代码)。数据结构也发生了一些改变,set和get方法均是直接对cachedata进行操作,exptime为过期时间,已经整合进这个数据结构中,判断也全部封装在这个类中了。 如果调用get,遇到了过期的数据,这里并不直接做删除处理,而是直接返回null,因为外部调用者很可能接下来会做缓存赋值set操作。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Timers;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using XXX.Models;
namespace XXX
{
// 注意: 有关启用 IIS6 或 IIS7 经典模式的说明,
// 请访问 http://go.microsoft.com/?LinkId=9394801
public class WebApiApplication : System.Web.HttpApplication
{
Timer tmClearCache = new Timer(600000);
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
tmClearCache.Elapsed += tmClearCache_Elapsed;
tmClearCache.Start();
}
void tmClearCache_Elapsed(object sender, ElapsedEventArgs e)
{
CacheContainer.ClearUp();
}
}
}
接下来是Invoker,也做了一些修改,增加了一些可选参数:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using XXX.Models;
using System.Reflection;
using System.Text;
namespace XXX.Filter
{
/// <summary>
/// 调用操作的工具类
/// </summary>
public static class ActionInvoker
{
public static T Invoke<T>(object target,string actionName, Func<T> method, object @params = null, bool cancel = false)
{
if (cancel)
{
return method.Invoke();
}
var targetType = target.GetType();
var targetInfo = targetType.GetMethod(actionName);
if (targetInfo.IsDefined(typeof(DataCacheAttribute), true))
{
var attr = targetInfo
.GetCustomAttributes(typeof(DataCacheAttribute), true)
.OfType<DataCacheAttribute>()
.FirstOrDefault();
StringBuilder sbKey = new StringBuilder();
sbKey.Append(targetType.Name);
sbKey.Append("-");
sbKey.Append(actionName);
if (@params != null)
{
var pros = @params.GetType().GetProperties();
foreach (var pro in pros.OrderBy(item => item.Name))
{
var value = pro.GetValue(@params);
if (value != null)
{
sbKey.Append("-");
sbKey.Append(pro.Name);
sbKey.Append("-");
sbKey.Append(value.ToString());
}
}
}
string key = sbKey.ToString();
T data = CacheContainer.GetData<T>(key);
if (data != null)
{
return (T)data;
}
else
{
var rsl = method.Invoke();
CacheContainer.SetData(key, rsl, attr.CacheSeconds);
return rsl;
}
}
else
{
return method.Invoke();
}
}
}
}
这里的@params为object类型的参数列表(也可以做成数组类型或者键值对类型等),是可选的,默认为null。 cancel为取消缓存标记,可以传递简单的逻辑表达式来选择是否跳过缓存,以适应同一个action下不同的缓存策略。 这里通过反射获取@params的全部属性名和值,然后拼成标识字串key,用于存取缓存数据。 这里,调用method.Invoke()时,也可以传递参数列表。只是由于我使用()=>{}形式的匿名函数,函数执行时上下文不用重新定义,所有参数均可从上下文获得,所以这里没有将参数传给method,参数列表在这里仅仅作为计算标识符key用。
下面我们看看调用,为了体现这货的用法,我选了个比较复杂的apicontroller的get方法:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using XXX.Models.Results;
using XXX.Models;
using System.Web;
using System.Text.RegularExpressions;
using XXX.Common;
using XXX.Filter;
namespace XXX.Controllers
{
public class ComplaintController : ApiController
{
XXXEntities prdb = new XXXEntities();
[DataCache]
public ComplaintListResult Get()
{
ComplaintListResult rsl = new ComplaintListResult();
var request = HttpContext.Current.Request;
int pageIndex = 1;
if (request.QueryString["pindex"] != null)
{
try
{
pageIndex = int.Parse(request.QueryString["pindex"]);
}
catch
{
rsl.success = false;
rsl.error_msg = "参数不正确";
return rsl;
}
}
int pageSize = 20;
if (request.QueryString["psize"] != null)
{
try
{
pageSize = int.Parse(request.QueryString["psize"]);
}
catch
{
rsl.success = false;
rsl.error_msg = "参数不正确";
return rsl;
}
}
bool selStatus = (request.QueryString["status"] != null);
bool selUser = (request.QueryString["phone"] != null);
if (selStatus && selUser)
{
rsl.success = false;
rsl.error_msg = "参数过多";
return rsl;
}
int status = 0;
if (selStatus)
{
try
{
status = Convert.ToInt32(request.QueryString["status"]);
}
catch
{
rsl.success = false;
rsl.error_msg = "参数不正确";
return rsl;
}
}
string phone = null;
if (selUser)
{
phone = request.QueryString["phone"];
}
return ActionInvoker.Invoke<ComplaintListResult>(this, "Get", () =>
{
int allCount = 0;
if (selStatus)
{
allCount = prdb.XXX.Count(item => item.ispassed == 0 && item.status == status && (item.visible ?? false));
}
else if (selUser)
{
allCount = prdb.XXX.Count(item => item.user_phone == phone);
}
else
{
allCount = prdb.XXX.Count(item => item.ispassed == 0 && (item.visible ?? false));
}
rsl.all_count = allCount;
//基于0based的pageindex来计算
pageIndex--;
if (pageIndex < 0)
{
pageIndex = 0;
}
if (pageSize < 1)
{
pageSize = 1;
}
if (pageSize * pageIndex >= allCount)
{
pageIndex = (allCount - 1) / pageSize;
}
int startIndex = pageIndex * pageSize;
int endIndex = startIndex + pageSize - 1;
if (endIndex > allCount - 1)
{
endIndex = allCount - 1;
}
rsl.all_list = new List<ComplaintListResult.ComplaintListItem>();
IQueryable<XXX> prelist = null;
if (selStatus)
{
prelist = prdb.XXX.Where(item => item.ispassed == 0 && item.status == status && (item.visible ?? false));
}
else if (selUser)
{
prelist = prdb.XXX.Where(item => item.user_phone == phone);
}
else
{
prelist = prdb.XXX.Where(item => item.ispassed == 0 && (item.visible ?? false));
}
if (prelist.Any())
{
var all_list = prelist.OrderByDescending(item => item.subtime).Skip(startIndex).Take(endIndex - startIndex + 1);
foreach (var i in all_list)
{
ComplaintListResult.ComplaintListItem i2 = new ComplaintListResult.ComplaintListItem
{
id = i.id,
summary = i.summary,
status = i.status,
subtime = (i.subtime == null) ? "" : ((DateTime)i.subtime).ToString("yyyy-MM-dd HH:mm:ss"),
title = i.title,
target = i.target,
user_name = i.user_name,
comment_count = i.comment_count ?? 0,
is_hot = i.comment_count >= CommonDefine.HotCountLimit
};
if (i.status >= 1 && i.unit_id != null)
{
i2.unit_name = i.XXXunit.name;
i2.unit_logo = i.XXXunit.logo;
}
rsl.all_list.Add(i2);
}
}
rsl.success = true;
return rsl;
}, new { pindex = pageIndex, psize = pageSize, status = (selStatus) ? (int?)status : null }, selUser);
}
}
}
这里返回的rsl,是XXX.Models.Results命名空间下定义的一个类,如下:
namespace XXX.Models.Results
{
public class ComplaintListResult
{
public bool success;
public string error_msg;
public class ComplaintListItem
{
public int id;
public string summary;
public string user_name;
public string subtime;
public int? status;
public string title;
public string target;
public int comment_count;
public bool is_hot;
public string unit_name;
public string unit_logo;
}
public int all_count;
public List<ComplaintListItem> all_list;
}
}
看看参数,这里涉及两个绝对可选参数和两个可选但是互斥参数,绝对可选参数为pindex表示当前页码,默认为0;psize表示页面大小,默认是20; 互斥可选参数为phone和status,这两个可以都没有,但是如果有,status必须是int型,同时phone和status不能同时存在。status和phone均是筛选条件,status存在时为筛选出状态码为status的数据,并分页,phone存在时,表示筛选出所有phone的值为传入的phone的所有数据。 究其用途,status较少,大约只有3-4种,但是phone与用户一一绑定,是海量的。所以,我们希望,对不存在status和phone以及只存在status的情况(这里pindex和psize可以任意,都会缓存)做出缓存,而对提供了phone的不做任何缓存。
具体流程可以不用特别关心,只需关心return ActionInvoker.Invoke()这一句。后面添加了两个可选参数,一个是匿名类型的object,用于传递参数,这里传递pindex,psize和status三个参数,这三个参数都可能是null,如果是null,从invoker的处理代码可以看到,不会添加作为key的标识。 但是也只是这3个字段,如果phone不为null,也就是selUser为true,则不作任何缓存。所以这里在参数列表对象后面又添加了一个bool类型的取消缓存标记selUser。告诉Invoker如果有phone参数提供,则不作缓存。
不妨亲自一试。代码表直接粘,直接粘是跑8起来滴。