☰
Guzzle 3 HTTP CachePlugin 深度实战:透明代理缓存的配置、再验证与扩展机制详解
2026/10/15 20:51:26 网站建设 项目流程
  • 后端
  • 电商
  • 前端

【免费下载链接】CRMEB

🔥CRMEB开源商城免费开源多语言商城系统,Tp6框架商城,系统可商用;包含小程序商城、H5商城、公众号商城、PC商城、App,支持分销、拼团、砍价、秒杀、优惠券、积分、会员等级、小程序直播、页面DIY,前后端分离,方便二开,更有详细使用文档、接口文档、数据字典、二开文档/视频教程,欢迎大家提出宝贵意见和建议

项目地址:https://gitcode.com/gh_mirrors/cr/CRMEB
点击查看免费下载

本篇技术指南围绕 CRMEB 仓库中打包的 cache-plugin.rst 文档展开,系统讲解 Guzzle 3 的Guzzle\Plugin\Cache\CachePlugin:从插件装配、缓存键生成、Vary请求区分,到cache.override_ttl、cache.key_filter、cache.revalidate等全部参数,再深入到条件 GET 再验证(revalidation)与 Doctrine/Zend 缓存适配器的底层实现。读完本文,你将能够在任何基于 Guzzle 3 的 PHP 项目中独立配置一个符合 RFC 2616 语义的 HTTP 响应缓存层,并能基于源码定位和定制缓存行为。

说明:本文讨论的CachePlugin属于Guzzle 3(vendor/guzzle/guzzle)这一代实现。CRMEB 主项目在 crmeb/composer.json 中声明的 HTTP 客户端为guzzlehttp/guzzle: 6.5.5,而 Guzzle 3 代码与文档仍作为依赖保留在crmeb/vendor/guzzle/guzzle目录中,其中的缓存插件文档与实现是一份完整的、可独立运行的参考(如Guzzle\Http\EntityBody仍在 QrcodeServices.php 与 StoreOrderServices.php 中被引用)。文中所指路径均相对当前仓库根目录。

一、CachePlugin 是什么:一个"私有透明代理缓存"

Guzzle\Plugin\Cache\CachePlugin是 Guzzle 3 提供的 HTTP 缓存插件。它实现了一个private transparent proxy cache(私有透明代理缓存):对应用而言它是透明的——代码不用关心缓存细节,只负责发请求、收响应;但底层它会把可缓存的 HTTP 响应保存下来,在后续请求中直接复用,从而减少网络往返、降低源站压力。

它的缓存判定逻辑严格遵循RFC 2616(HTTP/1.1 缓存语义),完全使用 HTTP 头部来控制:

  • 是否可以缓存、缓存多长时间,由响应的Cache-Control、Expires等头部决定;
  • 支持Vary头实现同一 URL 下的多版本缓存条目;
  • 支持基于ETag与Last-Modified的再验证(revalidation),用条件 GET 与源站确认内容是否变化。

在 CachePlugin.php 的类注释中,作者还明确指出该插件同时实现了RFC 5861 的stale-if-errorCache-Control 扩展:当请求遇到错误(如500 Internal Server Error、DNS 失败等 cURL 异常)时,允许返回已经过期的缓存响应兜底。

二、快速开始:把缓存插件挂到 Client 上

原文档给出的最小可运行示例(本节予以完整保留并补充注释):

use Guzzle\Http\Client; use Doctrine\Common\Cache\FilesystemCache; use Guzzle\Cache\DoctrineCacheAdapter; use Guzzle\Plugin\Cache\CachePlugin; use Guzzle\Plugin\Cache\DefaultCacheStorage; $client = new Client('http://www.test.com/'); $cachePlugin = new CachePlugin(array( 'storage' => new DefaultCacheStorage( new DoctrineCacheAdapter( new FilesystemCache('/path/to/cache/files') ) ) )); // 将缓存插件作为事件订阅者挂到客户端 $client->addSubscriber($cachePlugin); // 第一次请求:未命中缓存,直接访问源站并写入缓存 $client->get('http://www.wikipedia.org/')->send(); // 第二次请求:缓存已存在,向源站发起条件请求做再验证; // 若源站返回 304,则直接从缓存中返回合并后的响应 $client->get('http://www.wikipedia.org/')->send();

这段代码揭示了三个核心步骤:

  1. 选择后端存储:FilesystemCache(文件系统缓存)→DoctrineCacheAdapter(适配器)→DefaultCacheStorage(Guzzle 的缓存存储实现)。
  2. 构造插件:通过'storage'选项把存储对象注入CachePlugin。
  3. 订阅事件:addSubscriber($cachePlugin)使插件开始监听请求生命周期事件。

值得注意的是CachePlugin构造函数的灵活性:它既接受完整配置数组,也接受单独的适配器或存储对象。从 CachePlugin.php 可以看到构造器会按以下优先级兜底:

  • 传入CacheAdapterInterface→ 自动包装为DefaultCacheStorage;
  • 传入CacheStorageInterface→ 直接使用;
  • 传入其他已知缓存对象 → 通过CacheAdapterFactory::fromCache()自动适配;
  • 什么都不传,且环境中存在Doctrine\Common\Cache\ArrayCache→ 使用内存数组缓存(进程内有效,适合测试)作为默认存储;
  • 若两者都没有,则抛出InvalidArgumentException。

三、请求拦截与缓存查找流程

原文档描述了插件的工作流程:拦截 GET 和 HEAD 请求 → 基于请求方法与 URL 生成哈希键 → 检查缓存 → 校验缓存规则与新鲜度 → 命中则直接返回。这一流程在源码中以四个事件订阅点实现(见 CachePlugin.php):

订阅事件回调方法职责
request.before_sendonRequestBeforeSend发送前查缓存:命中且满足请求约束则直接注入缓存响应,跳过网络请求
request.sentonRequestSent响应到达后,若可缓存则写入存储,并给响应附加缓存状态头
request.erroronRequestError请求出错(如收到 5xx)时,按stale-if-error规则尝试返回过期缓存
request.exceptiononRequestExceptioncURL 层异常(如连接失败)时同样尝试用过期缓存兜底

3.1 缓存键的生成

缓存键由DefaultCacheStorage::getCacheKey()计算(见 DefaultCacheStorage.php):

return $this->keyPrefix . md5($request->getMethod() . ' ' . $url);

即keyPrefix+md5(HTTP方法 + ' ' + URL)。这意味着 GET 与 HEAD 各自独立缓存条目;同一 URL 下的多个响应(如不同Accept头对应的变体)则由Vary机制在同一个键下区分(详见下文)。

3.2 命中判定与缓存状态头

在onRequestBeforeSend中(CachePlugin.php),插件会:

  1. 给请求附加Via头,标识请求经过了GuzzleCache;
  2. 若命中缓存,则基于响应Date/Last-Modified计算并设置Age头;
  3. 调用canResponseSatisfyRequest()校验新鲜度与再验证需求;
  4. 满足条件后设置cache.hit参数并直接把缓存响应设为请求的响应。

在addResponseHeaders()(CachePlugin.php)中,插件会向响应追加一组标准的缓存调试头,便于观测缓存行为:

  • X-Cache-Lookup: HIT/MISS from GuzzleCache——本次请求是否在缓存中查到了响应;
  • X-Cache: HIT from GuzzleCache/MISS from GuzzleCache——是否直接命中;出错兜底时为HIT_ERROR from GuzzleCache;
  • Warning: 110 ... "Response is stale"——响应已过期但仍被使用时,按 RFC 2616 给出警告;111 ... "Revalidation failed"表示再验证失败后使用了缓存。

这些头在 CachePluginTest.php 的testInjectsSatisfiableResponses测试中均有断言验证。

3.3 新鲜度校验的判定矩阵

canResponseSatisfyRequest()(CachePlugin.php)按 RFC 2616 语义依次判断:

  • 请求携带Cache-Control: max-age=N时,响应年龄大于 N 则判定不可满足;
  • 响应已过期(isFresh() === false)时,检查请求的max-stale指令——若允许过期且未超出容忍范围,仍可命中;
  • 否则若响应的Cache-Control: max-age=N已过期,判定不可满足;
  • 最后交由再验证策略决定是否需要条件请求。

对应测试数据见 CachePluginTest.php 的satisfyProvider:例如max-age=20的请求面对年龄 100 的响应判为不可满足;max-stale=15的请求面对过期 10 秒的响应则判为可满足。这一矩阵直接决定缓存命中率与一致性之间的平衡。

四、Vary:为同一 URL 保存多个缓存变体

原文档指出:缓存键只由请求方法与 URL 决定,因此多个响应可以映射到同一个缓存键。为了区分同一 URL 下不同请求头产生的不同内容,必须使用VaryHTTP 头。

在DefaultCacheStorage中,每个缓存条目都会记录响应携带的Vary值,并在存取时用requestsMatch()(DefaultCacheStorage.php)逐一比对请求头与缓存条目中记录的Vary头字段;只要任一指定头部不一致,就会选择/创建不同的缓存条目。因此:

  • 响应声明Vary: Accept时,Accept: application/json与Accept: application/xml的请求会得到各自的缓存副本;
  • 不声明Vary时,相同 URL 的请求共享同一个缓存条目。

DefaultCacheStorageTest.php 的testCachesMultipleRequestsWithVary验证了同一缓存键下可以并存两条不同Accept的条目,且它们的内容、Content-Type与响应体键(bodyDigest)各不相同。

五、缓存选项详解:从请求级到客户端级

原文档给出了一批通过请求params对象控制的运行时选项,以下逐一结合源码展开。

5.1 覆盖缓存 TTL(cache.override_ttl)

默认情况下,TTL 由响应头决定;但当响应未携带任何新鲜度策略时,DefaultCacheStorage构造器会使用默认 3600 秒(见 DefaultCacheStorage.php)。

通过请求参数可以强制覆盖 TTL:

// 若该响应可缓存,则缓存 100 秒 $request->getParams()->set('cache.override_ttl', 100);

其底层读取逻辑位于DefaultCacheStorage::cache()(DefaultCacheStorage.php):优先级为cache.override_ttl> 响应Cache-Control: max-age> 默认 TTL(3600 秒);若响应还带stale-if-error指令,TTL 还会按指令值追加(stale-if-error为布尔真时 TTL 翻倍,为数值时加上该秒数)。

5.2 自定义缓存决策(can_cache)

当对接的服务既不返回缓存头、又返回常规情况下不应缓存的响应时,可以在构造插件时传入自定义的can_cache对象,其类型为Guzzle\Plugin\Cache\CanCacheInterface。最便捷的方式是使用Guzzle\Plugin\Cache\CallbackCanCacheStrategy,直接以闭包/回调决定某个请求或响应是否可缓存(CallbackCanCacheStrategy.php):

$plugin = new CachePlugin(array( 'can_cache' => function ($request) { // 例如:仅缓存带 token 的登录态请求 return $request->hasHeader('X-Api-Key'); } ));

从构造逻辑看(CachePlugin.php):若can_cache是 callable,会自动包装为CallbackCanCacheStrategy;否则按CanCacheStrategyInterface对象使用。默认策略DefaultCanCacheStrategy(DefaultCanCacheStrategy.php)只允许:

  • 缓存GET 与 HEAD请求(其他方法一概不缓存);
  • 请求带Cache-Control: no-store时不缓存;
  • 响应必须成功(2xx)且canCache()(即具有可缓存性,未声明no-store、no-cache等)时才写入缓存。

5.3 再验证行为控制(cache.revalidate)

通过cache.revalidate参数可以改变请求的再验证行为:

  • 设置为never:绝不发送再验证请求,总是从源站获取最新响应;
  • 设置为skip:不进行再验证,始终直接使用缓存中的响应。

这两个语义在源码中对应两个内置策略类——SkipRevalidation.php(revalidate()恒返回true,即"总是认为缓存有效")与 DenyRevalidation.php(revalidate()恒返回false,即"总是认为缓存无效")。原文档中的never与skip正是这两种策略在参数层的映射。

5.4 缓存键过滤(cache.key_filter)

当请求 URL 携带的查询字符串参数每次都不相同(例如时间戳、随机数),会导致每个请求 URL 唯一而永远无法命中缓存。此时可用cache.key_filter在生成缓存键前剔除指定查询参数:

// 生成缓存键时忽略 a 与 q 两个查询参数 $request->getParams()->set('cache.key_filter', 'a, q');

格式为逗号分隔的查询参数名列表。源码实现位于DefaultCacheStorage::getCacheKey()(DefaultCacheStorage.php):先explode(',', $filter)逐个trim后从 URL 查询串中移除,再计算 MD5 缓存键。DefaultCacheStorageTest.php 的testCanFilterCacheKeys展示了典型场景:请求带?auth=foo时本无法命中,设置cache.key_filter = 'auth'后即可命中。

5.5 其他构造器选项与 Client 级全局配置

原文档指出,CachePlugin构造器还支持自定义再验证实现、自定义缓存键生成器、自定义缓存决策策略与自定义缓存存储对象等扩展点。综合构造器注释(CachePlugin.php),完整的选项表如下:

选项类型默认值说明
storageCacheStorageInterface内存ArrayCache包装的DefaultCacheStorage实际存储响应的对象
revalidationRevalidationInterfaceDefaultRevalidation再验证策略
can_cachecallable 或CanCacheInterfaceDefaultCanCacheStrategy是否允许缓存请求/响应的决策
auto_purgeboolfalse发送 PUT/POST/DELETE/PATCH 等非幂等请求时是否自动 PURGE 对应 URL 的缓存

此外,可以通过 Client 的配置选项为该客户端创建的所有请求统一设置缓存参数(原文档示例,完整保留):

$client = new Guzzle\Http\Client('http://www.test.com', array( 'request.params' => array( 'cache.override_ttl' => 3600, 'params.cache.revalidate' => 'never' ) )); echo $client->get('/')->getParams()->get('cache.override_ttl'); // >>> 3600 echo $client->get('/')->getParams()->get('cache.revalidate'); // >>> never

这样新创建的请求会自动继承这些params,实现全局缓存策略。

六、缓存再验证(Revalidation)机制剖析

这是原文档的另一核心主题。当缓存插件判定一个 GET 请求需要再验证时,会向源站发送条件 GET(conditional GET);源站返回304 Not Modified时,返回由缓存响应与新响应合并后的头加上缓存实体内容组成的响应。

DefaultRevalidation.php 完整实现了这套流程:

1. 判定是否需要再验证(shouldRevalidate,第 49-68 行)。以下任一条件成立则触发再验证:

  • 请求带Pragma: no-cache;
  • 请求或响应带Cache-Control: no-cache/must-revalidate;
  • 响应没有 Cache-Control 头但带有ETag(此时按强验证器强制再验证)。

2. 构造条件请求(createRevalidationRequest,第 95-119 行):

  • 克隆原请求,移除Pragma与Cache-Control,避免再次触发缓存逻辑;
  • 缓存响应有Last-Modified则设置If-Modified-Since头;
  • 缓存响应有ETag则设置If-None-Match头;
  • 从事件分发器中移除CachePlugin监听器,防止再验证请求触发无限递归的再验证。

3. 处理源站响应:

  • 返回200:直接以新响应作为请求响应,若新响应可缓存则更新缓存(handle200Response,第 129-137 行)——对应原文档"服务器不支持验证,直接采用新响应";
  • 返回304:校验ETag一致后,用新响应的Date、Expires、Cache-Control、ETag、Last-Modified五个头替换缓存中的旧头,再写回缓存并返回缓存实体(handle304Response,第 148-173 行)——对应原文档"合并头 + 缓存实体";
  • 返回404:资源已不存在,删除缓存条目并抛出异常(handleBadResponse,第 77-85 行)。

DefaultRevalidationTest.php 的cacheRevalidationDataProvider给出了四组端到端用例:Pragma: no-cache强制再验证且 304 通过、must-revalidate, no-cache时 200 覆盖缓存、再验证遇到 500 时回退、以及 304 但ETag不一致时判定失败。testRevalidatesResponsesAgainstOriginServer还验证了三次请求只产生 3 次真实网络请求,其中后两次均由 304 兜住,直接证明了缓存的节省效果。

七、缓存适配器:Guzzle 不重复造轮子

原文档强调:Guzzle 在缓存与日志方面复用成熟生态,通过适配器接入既有的缓存方案。缓存插件需要一个缓存适配器(cache adapter)来存储响应,Guzzle 3 原生支持Doctrine 2.0与Zend Framework两套缓存后端。

7.1 Doctrine 缓存适配器

use Doctrine\Common\Cache\ArrayCache; use Guzzle\Cache\DoctrineCacheAdapter; use Guzzle\Plugin\Cache\CachePlugin; $backend = new ArrayCache(); $adapter = new DoctrineCacheAdapter($backend); $cache = new CachePlugin($adapter);

这里直接把适配器作为CachePlugin的唯一构造参数,插件会自动包装成DefaultCacheStorage。DoctrineCacheAdapter.php 只是把 Guzzle 的fetch/save/delete/contains接口转发给 Doctrine 的Cache接口。Doctrine\Common\Cache\ArrayCache适合内存缓存;快速开始中的FilesystemCache则提供跨请求的文件级持久化。Doctrine 缓存家族(RedisCache、MemcachedCache、ApcuCache等)均可通过该适配器接入。

7.2 Zend Framework 缓存适配器

use Guzzle\Cache\ZendCacheAdapter; use Zend\Cache\Backend\TestBackend; $backend = new TestBackend(); $adapter = new ZendCacheAdapter($backend); $cache = new CachePlugin($adapter);

对应 Zend Framework 的缓存后端(测试中常用TestBackend,生产可换用File、Memcached等后端)。

7.3 自动适配入口:CacheAdapterFactory

如果不想手动包一层,CacheAdapterFactory::fromCache()(CacheAdapterFactory.php)会根据传入对象自动选择:

  • 已是CacheAdapterInterface→ 直接返回;
  • 是 Doctrine 的Cache→ 包装为DoctrineCacheAdapter;
  • 是 Zend 的StorageInterface→ 包装为Zf2CacheAdapter;
  • 其他类型 → 抛出InvalidArgumentException。

因此new CachePlugin($someDoctrineCache)这类简化写法在底层都能正确展开。

八、实战建议与注意事项

结合原文档与源码,给出几条可直接落地的实践结论:

  1. 明确适用边界:该插件设计为"私有透明代理缓存",会缓存授权与私有数据(见 CachePlugin.php 注释),因此对用户私有接口务必通过can_cache决策或Cache-Control: no-store关闭缓存,避免敏感数据泄漏。
  2. 善用缓存调试头:上线后通过X-Cache/X-Cache-Lookup/Warning头可以快速判断 HIT/MISS/过期兜底三种状态,配合Age头观察缓存寿命。
  3. 过滤"搅局"查询参数:凡 URL 带有时间戳、随机数、登录态 token 等唯一化参数,都应配置cache.key_filter,否则缓存形同虚设。
  4. 理解再验证的价值:304 再验证是缓存一致性关键——它既避免重复传输完整实体,又保证内容更新后能及时拿到新数据;ETag变化或must-revalidate都会强制再验证。
  5. 失效管理:auto_purge开启后,非幂等请求(PUT/POST/DELETE/PATCH)会自动清理对应 URL 的缓存条目;也可手动调用$cachePlugin->purge($url),DefaultCacheStorage::purge()会同时清除 GET/HEAD/POST/PUT/DELETE 五种方法的缓存条目(DefaultCacheStorage.php)。
  6. 运行环境前提:本插件代码基于 PHP 5.3+ 与 Guzzle 3(见 composer.json),适配器依赖doctrine/cache、zendframework/zend-cache等扩展包;当前仓库仅保留了 Guzzle 3 的代码与文档,实际项目若要使用,应确认依赖已被正确引入。

九、总结

Guzzle\Plugin\Cache\CachePlugin为 Guzzle 3 提供了完整的 HTTP 缓存能力:事件驱动的请求拦截、基于方法与 URL 的哈希缓存键、Vary多变体存储、按 RFC 2616 语义的新鲜度判定、基于ETag/Last-Modified的条件请求再验证,以及通过适配器对接 Doctrine/Zend 等成熟缓存后端。本文所有运行细节均可在 src/Guzzle/Plugin/Cache 源码与 tests/Guzzle/Tests/Plugin/Cache 测试中找到对应的实现证据与端到端用例,是深入理解乃至二次定制该缓存插件的可靠起点。

  • 后端
  • 电商
  • 前端

【免费下载链接】CRMEB

🔥CRMEB开源商城免费开源多语言商城系统,Tp6框架商城,系统可商用;包含小程序商城、H5商城、公众号商城、PC商城、App,支持分销、拼团、砍价、秒杀、优惠券、积分、会员等级、小程序直播、页面DIY,前后端分离,方便二开,更有详细使用文档、接口文档、数据字典、二开文档/视频教程,欢迎大家提出宝贵意见和建议

项目地址:https://gitcode.com/gh_mirrors/cr/CRMEB
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询