Laravel/Lumen中如何编程实现Guzzle指定缓存目录失效刷新
我在PHP ^8.1.3环境的Lumen ^9.0项目(底层机制与Laravel一致)中,使用guzzlehttp/guzzle ^7.4作为HTTP客户端,搭配kevinrob/guzzle-cache-middleware ^4.0、symfony/cache ^6.1组件实现请求文件缓存。
缓存按业务分目录存放,例如书籍价格接口https://example.com/books/prices的缓存路径为framework/cache/data/GuzzleFileCache/public/books/prices,缓存TTL为1小时,调用示例如下:
curl -X GET https://example.com/books/prices
若后台提前更新了书籍价格,缓存TTL未过期时接口会返回旧价格;我已实现价格更新时触发事件通知主服务的逻辑,需要在事件触发时主动失效对应路径的缓存。
当前缓存初始化配置代码如下:
$ttl = 3600; // 创建HandlerStack实例 $stack = HandlerStack::create(); // 指定缓存根目录名 $requestCacheFolderName = 'framework/cache/data'; // 指定业务缓存子路径 $cacheFolderPath = 'GuzzleFileCache/public/blogs'; // 实例化基于PSR-6规范的文件缓存存储 $cache_storage = new Psr6CacheStorage( new FilesystemAdapter( $requestCacheFolderName, $ttl, $cacheFolderPath ) ); // 挂载缓存中间件到调用栈 $stack->push( new CacheMiddleware( new GreedyCacheStrategy( $cache_storage, $ttl // TTL单位为秒 ) ), 'greedy-cache' );
需要支持编程式定向清除/刷新指定业务路径(如framework/cache/data/GuzzleFileCache/public/blogs、framework/cache/data/GuzzleFileCache/public/books/prices)下的Guzzle缓存。目前已知可通过删除目录的粗暴方式实现:
rmdir('framework/cache/data/GuzzleFileCache/public/blogs');
但需要更规范的实现方案,达到类似下面向调用的效果,无需手动操作文件系统删除目录:
use Illuminate\Support\Facades\Http; // Laravel/Lumen封装的Guzzle客户端 Http::cacheInvalidate('framework/cache/data/GuzzleFileCache/public/blogs');
项目依赖
{ "php": "^8.1.3", "flipbox/lumen-generator": "^9.1", "guzzlehttp/guzzle": "^7.4", "kevinrob/guzzle-cache-middleware": "^4.0", "laravel/lumen-framework": "^9.0", "symfony/cache": "^6.1" }
实现方案
不要直接通过rmdir删除缓存目录,Symfony文件缓存组件维护了统一的缓存元数据索引,手动删目录会导致元数据与实际文件不一致,引发缓存失效异常、命中失败等问题。可以通过以下步骤实现规范的定向缓存清除:
- 第一步:将缓存实例注册为容器单例,全局复用
原有代码每次初始化Guzzle客户端都会新建缓存实例,后续清除缓存时无法拿到对应实例操作。可以在服务提供者或bootstrap/app.php中绑定缓存单例,注意修正原有代码中FilesystemAdapter的参数传值错误(原代码把根目录和命名空间参数传反了):
$defaultTtl = 3600; $cacheRootPath = storage_path('framework/cache/data'); $this->app->singleton('guzzle.cache.store', function () use ($defaultTtl, $cacheRootPath) { return new FilesystemAdapter( '', // 留空默认命名空间,方便后续按前缀批量清除 $defaultTtl, $cacheRootPath ); });
- 第二步:调整Guzzle客户端初始化逻辑,复用容器内的缓存实例
初始化Guzzle调用栈时直接从容器取缓存实例,避免重复创建:
$stack = HandlerStack::create(); $cacheStorage = new Psr6CacheStorage(app('guzzle.cache.store')); $stack->push(new CacheMiddleware(new GreedyCacheStrategy($cacheStorage, $ttl)), 'greedy-cache'); // 将$stack传入Guzzle客户端实例完成初始化即可
- 第三步:给Http门面注册缓存清除宏,实现预期调用效果
在服务提供者的boot方法中注册cacheInvalidate宏,底层调用Symfony缓存的原生前缀清除能力,无需手动操作文件:
use Illuminate\Support\Facades\Http; use Kevinrob\GuzzleCache\CacheMiddleware; use Symfony\Component\Cache\Adapter\FilesystemAdapter; Http::macro('cacheInvalidate', function (string $businessPath, string $baseUri = 'https://example.com/') { /** @var FilesystemAdapter $cacheStore */ $cacheStore = app('guzzle.cache.store'); // 从传入的业务路径中解析实际接口路径 $apiPath = trim(str_replace('framework/cache/data/GuzzleFileCache/public/', '', $businessPath), '/'); $targetUri = rtrim($baseUri, '/') . '/' . $apiPath; // 生成该路径对应的缓存key前缀,前缀规则与guzzle-cache-middleware内部生成逻辑一致 $keyPrefix = CacheMiddleware::CACHE_KEY_PREFIX . sha1('GET' . $targetUri); // 调用Symfony缓存原生清除方法,自动维护元数据一致性 $cacheStore->clear($keyPrefix); return true; });
- 第四步:业务侧直接调用即可
在价格更新的事件监听器、其他需要失效缓存的业务场景中,直接按预期方式调用:
// 清除博客模块所有接口缓存 Http::cacheInvalidate('framework/cache/data/GuzzleFileCache/public/blogs'); // 清除书籍价格接口缓存 Http::cacheInvalidate('framework/cache/data/GuzzleFileCache/public/books/prices');
补充说明:如果需要清除POST、PUT等其他请求方法的缓存,只需要调整宏里生成key前缀时传入的请求方法参数即可,默认场景下GET请求是最常用的缓存对象。
内容的提问来源于stack exchange,提问作者Federico Baù

