缓存是Web开发中的重要技术,当在开发RESTful服务也需要重视。合理的利用缓存可以大大提高服务的响应能力。从技术实现上,有客户端缓存和服务端缓存两大部分组成。而无论在哪边进行缓存,都需要一些数据来比较是否过期,Http协议中控制缓存的规则有:Cache-Control, ETag, Expires, Last-Modified。Expires是一种无条件缓存(通过过期时间控制),Last-Modified,ETag是一种有条件缓存(通过数据的标识(时间或者ID)来控制。
如果是无条件缓存客户端浏览器会检查Expires过期时间判断是否发出请求,如果是有条件客户端缓存,则会提交Last-Modified-Since或者ETag供服务端检查是否发生变化,返回304(Not Modified)提示客户端查询的数据没有变化,这需要客户端自己保留缓存。如果是服务端缓存,则返回200(OK)但数据从服务端自己的缓存中读取。另外通常说的缓存策略主要是针对查询即GET操作而言的。
不同的缓存策略有不同的适用场景,对于WCF REST来说客户端缓存需要在客户端额外的控制编码。服务端缓存则要考虑数据量尽量只对共享数据进行缓存,如果对于每个客户端的私有数据都缓存对存储空间来说是一个考验。
来看看有条件客户端缓存的示例:
服务端提供一个 GetTasks 方法,返回 Task 数组。一个 AddTask 方法,添加Task。每次添加Task都会修改_lastModifed(DateTime)
[ServiceContract]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public class Service1
{
private static List<Task> _tasks = null;
private static DateTime? _lastModified = null;
static Service1()
{
_tasks = new List<Task>
{
new Task { ID="001", Content="Task1", Title="Title1"},
new Task { ID="002", Content="Task2", Title="Title2"},
new Task { ID="003", Content="Task3", Title="Title3"},
};
_lastModified = DateTime.UtcNow;
}
[WebGet(UriTemplate="Tasks", ResponseFormat=WebMessageFormat.Json)]
public List<Task> GetTasks()
{
var req = WebOperationContext.Current.IncomingRequest;
var resp = WebOperationContext.Current.OutgoingResponse;
var modifiedSince = req.IfModifiedSince;
if (modifiedSince.HasValue)
req.CheckConditionalRetrieve(_lastModified.Value);
resp.LastModified = _lastModified.Value;
return _tasks;
}
[WebInvoke(UriTemplate = "NewTask", Method = "POST", RequestFormat = WebMessageFormat.Json)]
public void AddTask(Task task)
{
lock (_tasks)
{
task.ID = (_tasks.Count + 1).ToString("000");
_tasks.Add(task);
_lastModified = DateTime.UtcNow;
}
WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.Accepted;
}
//[WebGet(UriTemplate = "Tasks", ResponseFormat = WebMessageFormat.Json)]
//[AspNetCacheProfile("CacheFor5Seconds")]
//public List<Task> GetTasks()
//{
// return _tasks;
//}
}
public class Task
{
public string ID { get; set; }
public string Title { get; set; }
public string Content { get; set; }
}
通过浏览器连续调用2次:http://localhost:52533/Service1/tasks 用 Fiddler 拦截可以看到
第一次返回200, 第二次返回304而且没有Response Body (hoho,这样网络传输的代价也减少了)
上面的代码中:CheckConditionalRetrieve 方法的一个重载接受上次修改的日期并根据请求的 If-Modified-Since 标头检查该日期。如果该标头存在并且自该日期以来尚未修改资源,将引发 WebFaultException 并返回 HTTP 状态代码 304。而 CheckConditionalRetrieve 之后的“查询操作”(return _tasks)实际也就没有进行。
上面只是浏览器的行为结果,下面看看客户端如何模拟调用:
Tip: WebClient 中对于 Last-Modified-Since 有限制,因此无法使用 WebClient.Headers.Add() 方法来添加头信息。(会抛出异常)
var url = "http://localhost:20000/service1/tasks";
var req = WebRequest.Create(url) as HttpWebRequest;
req.Method = "GET";
if (!string.IsNullOrEmpty(lastModified))
req.IfModifiedSince = DateTime.Parse(lastModified);
var result = "";
using (var resp = req.GetResponse())
{
lastModified = resp.Headers["Last-Modified"];
using (var sr = new System.IO.StreamReader(resp.GetResponseStream()))
{
result = sr.ReadToEnd();
}
}
var tasks = JsonConvert.DeserializeObject<List<Task>>(result);
if (tasks == null) return;
tasks.ForEach(t => Console.WriteLine(t));
这段代码从Response里取得 Last-Modified,并添加到下一次请求的 Last-Modified-Since 中,如果服务端检查发现数据没有变化,则在 GetResponse() 时抛出 WebException (No Modify)。很显然这需要我们自己在客户端维护这样的数据,以便在发现 No Modify 时可以使用。
这无疑会加大客户端编码的复杂程度,我想实际运用时也可以把“数据没有发生变化”的异常抛给UI直接显示。
这里又引申出服务端缓存的概念,客户端无论如何都希望获得200(OK)的Response。在 WCF 4.0 里利用 ASP.NET 服务的兼容模式,还可以利用 [AspNetCacheProfile] 特性。上面的 GetTasks 方法修改如下:
[WebGet(UriTemplate = "Tasks", ResponseFormat = WebMessageFormat.Json)]
[AspNetCacheProfile("CacheFor5Seconds")]
public List<Task> GetTasks()
{
return _tasks;
}
CacheFor5Seconds 对应的在 Web.config 中加上配置:
<system.web>
<compilation debug="true" targetFramework="4.0" />
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="CacheFor5Seconds" duration="5" varyByParam="none" />
</outputCacheProfiles>
</outputCacheSettings>
</caching>
</system.web>
缓存配置文件中最重要的特性是 cacheDuration 和 varyByParam。这两个特性都是必需的。cacheDuration 设置应缓存响应的时间(以秒为单位)。使用 varyByParam 可指定用于缓存响应的查询字符串参数。对于使用不同查询字符串参数值发出的所有请求,将单独进行缓存。例如,对 http://MyServer/MyHttpService/MyOperation?param=10 发出初始请求后,使用同一 URI 发出的所有后续请求都将返回已缓存的响应(只要缓存持续时间尚未结束)。对于形式相同但具有不同参数查询字符串参数值的类似请求的响应,将单独进行缓存。如果不需要此单独缓存行为,请将 varyByParam 设置为“none”。
客户端:
// 获取Tasks
static void GetTasks()
{
var url = "http://localhost:20000/service1/tasks";
var client = new WebClient();
var result = client.DownloadString(url);
var tasks = JsonConvert.DeserializeObject<List<Task>>(result);
if (tasks == null) return;
tasks.ForEach(t => Console.WriteLine(t));
Console.WriteLine("------");
}
// 添加Tasks
static void AddTask()
{
var url = "http://localhost:20000/service1/NewTask";
var client = new WebClient();
var task = new Task { ID = "", Title = "Title_Test", Content = "Content_Test" };
var json = JsonConvert.SerializeObject(task);
client.Headers["Content-Type"] = "application/json";
client.UploadString(url, "POST", json);
}
为了证明服务端缓存的效果,我在1次GetTasks()之后,执行一次AddTask(),再每隔1秒进行一次GetTasks()连续两次,再隔4秒进行一次GetTasks()。这样,前3次都在AddTask()之后5秒内查询,最后一次是在AddTask()之后第6秒查询。
GetTasks();
AddTask();
Thread.Sleep(1000);
GetTasks();
Thread.Sleep(1000);
GetTasks();
Thread.Sleep(4000);
GetTasks();
运行结果:
可以看到,虽然AddTask成功了,但是在缓存期内,服务端并没有真正的执行“查询操作"(return _tasks),返回的只是缓存数据。