本章介绍通过使用Ali Kheyrollahi开发的CacheCow来实现服务器端的缓存。所有代码现在都可以在GitHub上下载。
我们将要实现的缓存方式叫做Conditional Requests,实现方式其实很简单。客户端通过在请求Header中包含ETag信息,实现和服务器的交互,确认当前客户端包含的部分数据是否已经被修改,有则拉取,没有服务器端就返回304(Not Modified)和空的消息体。通过Conditional Requests,客户端不断的发起请求,但只有客户端数据过期时才会返回200和对应的数据信息。
什么是ETag(Entity Tag)?
ETag有一个基于String的惟一键,服务端为每种类型的资源单独生成,可以看做是资源的校验位,用来控制对应的资源是否被修改。
ETag有强标识和弱标识两种,弱标识以W开头,形如“W/53fsfsd322”;强标识一般形如“534928jhfr4”。弱标识一般标明缓存的资源只会被使用一小段时间(In Momory Caching)。而强标识则意味着资源会被持久化,资源在客户端和服务端都是完全相同(字节相等,byte-per-byte identical)的。(注:留待查明,作者的说法可能有问题,文章最后提到了一些不同的观点)
ETag的原理
下图展示了一个过程,当客户端通过HTTP GET请求Id为4的course资源时,如果该资源从未被请求,那么服务端直接返回该资源并在Response Header中添加了一个ETag标识。
如果客户端再次请求统一资源,可以再GET请求中包含一个叫做If-None-Match的Header,其值为对应的ETag。当服务器收到时,就会进行匹配。如果未改动,返回304和空的消息体。否则,返回最新的记录。
GET和DELETE都可以使用这个If-None-Match的Header,但是更新资源时如果要用到ETag,需要在对应的PUT或者PATCH请求中使用If-Match的Header,服务器同样会对ETag进行检查。如果服务端资源信息已被更新,就会返回412(Precondition Failed),客户端需要进行必要的提示和处理,本次修改将不能被服务端接受。
配置CacheCow
从Nuget控制台可以通过命令Install-Package CacheCow.Server -Version 0.4.12获取到CacheCow的最新版本。包含两个dll文件。
配置的过程就是创建一个CacheHandler实例,并将其诸如到Web API的处理管线中,这个Handler将用来对每个请求进行拦截,并对Response进行必要的处理。
1: //Configure HTTP Caching using Entity Tags (ETags)
2: var cacheCowCacheHandler = new CacheCow.Server.CachingHandler();
3: config.MessageHandlers.Add(cacheCowCacheHandler);
到目前为止,我们的API可以实现基于内存的缓存,这也是CacheCow的默认配置。对于单个服务器或者演示,而言已经足够。但是如果要处于负载均衡或WebFarm状态时,缓存状态必须单独存储,并在不同的服务器间进行共享,因此需要配置额外的存储介质,如SQL Server,MongoDB,MemCache等。
基于内存的缓存测试
进行后续操作前,先对请求进行一个测试。打开Fiddler,选择Composer选项卡,构造一个GET请求,类似于:http://localhost:{your_port}/api/courses/4 ,请求的结果如下:
你应该注意到这些:
- 返回状态码为200,表示服务端在消息体返回了资源的内容;
- 有两个新的Header被添加到Response中;注意,后续我将介绍将Last-Modified头去掉以免互相影响;
- 图中的ETag是弱标识类型(以W开头),
标示数据缓存在内存中,一旦IIS重启,缓存会丢失。
接下来,对请求进行模拟。在Headers中添加If-None-Match,执行后的结果如下:
值得注意的是,返回的ETag标识和传入的标识是相同的。
通过SQL Server进行缓存
这种方式需要安装必要的dll,在NuGet控制台中通过“Install-Package CacheCow.Server.EntityTagStore.SqlServer -Version 0.4.11”进行安装,然后修改WebApiConfig文件如下:
1: //Configure HTTP Caching using Entity Tags (ETags)
2: var connString = System.Configuration.ConfigurationManager.ConnectionStrings["eLearningConnection"].ConnectionString;
3: var eTagStore = new CacheCow.Server.EntityTagStore.SqlServer.SqlServerEntityTagStore(connString);
4: var cacheCowCacheHandler = new CacheCow.Server.CachingHandler(eTagStore);
5: cacheCowCacheHandler.AddLastModifiedHeader = false;
6: config.MessageHandlers.Add(cacheCowCacheHandler);
我们向CacheHandler中传递了数据库的信息,并去掉了LastModifiedHeader。如果你在这个时候测试请求,会接收到一个500错误(Internal Server Error),因为我们还需要创建必要的存储过程和表。定位到NuGet包的位置,一般类似于“{projectpath}packagesCacheCow.Server.EntityTagStore.SqlServer.0.4.11scripts”,然后再数据库中执行即可。
进行上文类似的测试后,检索CacheState表,可以看到如下信息:
接下来再发生的GET请求都会直接返回304,知道数据被修改。现在我们接着对数据进行Update,返回情况如下图。
这个PUT请求分析如下:
- 返回状态码为200,意味着客户端的数据是最新的,并且更新也被正确的执行了;
- 返回了新的ETag,因为服务端的数据已经被修改;客户端接下来的访问都必须使用新的ETag;
- CacheStore记录了新的ETag值和Last Modified Date。
如果再次执行刚才的请求,将会直接返回412错误。
来自评论和思考
简单说,这种缓存方式对单个或少量的API十分有效。但是,如果存在拉取一个大的列表如/api/courses这样,就会比较难以处理。特别的,请求返回时,对逐个对象进行判断也是十分费事的。作者建议对此类的API关闭缓存,然后通过AttributeBasedCancheControlPolicy对单一的请求进行标识。
另外,有个人提到W/其实并不标识缓存的存储介质类型,他用来标识数据是语义相等,而强标识则意味着字节相等。这个人还提出了一套基于ActionFilter的解决方案。
系列到此结束,完整代码现在都可以在GitHub上下载。
来源:http://bitoftech.net/2014/02/08/asp-net-web-api-resource-caching-etag-cachecow/