- 后端
- 电商
- 前端
【免费下载链接】CRMEB
🔥CRMEB开源商城免费开源多语言商城系统,Tp6框架商城,系统可商用;包含小程序商城、H5商城、公众号商城、PC商城、App,支持分销、拼团、砍价、秒杀、优惠券、积分、会员等级、小程序直播、页面DIY,前后端分离,方便二开,更有详细使用文档、接口文档、数据字典、二开文档/视频教程,欢迎大家提出宝贵意见和建议
本篇技术指南围绕 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();这段代码揭示了三个核心步骤:
- 选择后端存储:
FilesystemCache(文件系统缓存)→DoctrineCacheAdapter(适配器)→DefaultCacheStorage(Guzzle 的缓存存储实现)。 - 构造插件:通过
'storage'选项把存储对象注入CachePlugin。 - 订阅事件:
addSubscriber($cachePlugin)使插件开始监听请求生命周期事件。
值得注意的是CachePlugin构造函数的灵活性:它既接受完整配置数组,也接受单独的适配器或存储对象。从 CachePlugin.php 可以看到构造器会按以下优先级兜底:
- 传入
CacheAdapterInterface→ 自动包装为DefaultCacheStorage; - 传入
CacheStorageInterface→ 直接使用; - 传入其他已知缓存对象 → 通过
CacheAdapterFactory::fromCache()自动适配; - 什么都不传,且环境中存在
Doctrine\Common\Cache\ArrayCache→ 使用内存数组缓存(进程内有效,适合测试)作为默认存储; - 若两者都没有,则抛出
InvalidArgumentException。
三、请求拦截与缓存查找流程
原文档描述了插件的工作流程:拦截 GET 和 HEAD 请求 → 基于请求方法与 URL 生成哈希键 → 检查缓存 → 校验缓存规则与新鲜度 → 命中则直接返回。这一流程在源码中以四个事件订阅点实现(见 CachePlugin.php):
| 订阅事件 | 回调方法 | 职责 |
|---|---|---|
request.before_send | onRequestBeforeSend | 发送前查缓存:命中且满足请求约束则直接注入缓存响应,跳过网络请求 |
request.sent | onRequestSent | 响应到达后,若可缓存则写入存储,并给响应附加缓存状态头 |
request.error | onRequestError | 请求出错(如收到 5xx)时,按stale-if-error规则尝试返回过期缓存 |
request.exception | onRequestException | cURL 层异常(如连接失败)时同样尝试用过期缓存兜底 |
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),插件会:
- 给请求附加
Via头,标识请求经过了GuzzleCache; - 若命中缓存,则基于响应
Date/Last-Modified计算并设置Age头; - 调用
canResponseSatisfyRequest()校验新鲜度与再验证需求; - 满足条件后设置
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),完整的选项表如下:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
storage | CacheStorageInterface | 内存ArrayCache包装的DefaultCacheStorage | 实际存储响应的对象 |
revalidation | RevalidationInterface | DefaultRevalidation | 再验证策略 |
can_cache | callable 或CanCacheInterface | DefaultCanCacheStrategy | 是否允许缓存请求/响应的决策 |
auto_purge | bool | false | 发送 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)这类简化写法在底层都能正确展开。
八、实战建议与注意事项
结合原文档与源码,给出几条可直接落地的实践结论:
- 明确适用边界:该插件设计为"私有透明代理缓存",会缓存授权与私有数据(见 CachePlugin.php 注释),因此对用户私有接口务必通过
can_cache决策或Cache-Control: no-store关闭缓存,避免敏感数据泄漏。 - 善用缓存调试头:上线后通过
X-Cache/X-Cache-Lookup/Warning头可以快速判断 HIT/MISS/过期兜底三种状态,配合Age头观察缓存寿命。 - 过滤"搅局"查询参数:凡 URL 带有时间戳、随机数、登录态 token 等唯一化参数,都应配置
cache.key_filter,否则缓存形同虚设。 - 理解再验证的价值:304 再验证是缓存一致性关键——它既避免重复传输完整实体,又保证内容更新后能及时拿到新数据;
ETag变化或must-revalidate都会强制再验证。 - 失效管理:
auto_purge开启后,非幂等请求(PUT/POST/DELETE/PATCH)会自动清理对应 URL 的缓存条目;也可手动调用$cachePlugin->purge($url),DefaultCacheStorage::purge()会同时清除 GET/HEAD/POST/PUT/DELETE 五种方法的缓存条目(DefaultCacheStorage.php)。 - 运行环境前提:本插件代码基于 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,前后端分离,方便二开,更有详细使用文档、接口文档、数据字典、二开文档/视频教程,欢迎大家提出宝贵意见和建议
相关推荐
FRI-STARKs原理深入:理解可扩展透明论证的核心机制
FRI STARKs原理深入:理解可扩展透明论证的核心机制 FRI STARKs是现代零知识证明技术中的重要突破,代表了可扩展透明知识论证的最前沿。作为STAR
文档教程PyRDP透明代理技术详解与实战配置指南
PyRDP透明代理技术详解与实战配置指南 透明代理基础概念 透明代理(Transparent Proxy)是一种网络中间人技术,它能够在客户端和服务端无感知的情
网络安全渗透测试探索IReader:开源跨平台小说阅读器的创新体验
探索IReader:开源跨平台小说阅读器的创新体验 IReader是一款专为小说爱好者设计的开源阅读器,支持Android和桌面平台,提供无广告、隐私保护的阅读
移动开发桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考