nette/caching

⏱ Nette 缓存:具有易于使用的 API 和许多缓存后端的库。

v3.3.1 2024-08-07 00:01 UTC

README

Downloads this Month Tests Coverage Status Latest Stable Version License

介绍

缓存通过存储数据(一旦难以检索)来加速您的应用程序,以便将来使用。

文档可以在网站上找到。

支持我

你喜欢 Nette 缓存吗?你在期待新功能吗?

Buy me a coffee

谢谢!

安装

composer require nette/caching

它需要 PHP 版本 8.1,并支持 PHP 8.4。

基本用法

与缓存工作的中心是对象 Nette\Caching\Cache。我们创建其实例,并将所谓的存储作为参数传递给构造函数。这表示数据将实际存储的位置(数据库、Memcached、磁盘上的文件等)。你可以在存储部分找到所有基本内容。

对于下面的示例,假设我们有一个别名 Cache 和存储在变量 $storage 中。

use Nette\Caching\Cache;

$storage // instance of Nette\Caching\IStorage

实际上,缓存是一个 键值存储,所以我们像关联数组一样在键下读取和写入数据。应用程序由多个独立的部分组成,如果它们都使用一个存储(例如:磁盘上的一个目录),那么迟早会发生键冲突。Nette 框架通过将整个空间划分为命名空间(子目录)来解决这个问题。程序的每个部分随后使用其独特的空间和名称,从而不会发生冲突。

空间名称是作为 Cache 类构造函数的第二个参数指定的

$cache = new Cache($storage, 'Full Html Pages');

现在我们可以使用对象 $cache 从缓存中读取和写入。方法 load() 适用于两者。第一个参数是键,第二个是 PHP 回调,当键不在缓存中时调用。回调生成一个值,返回它并将其缓存在缓存中

$value = $cache->load($key, function () use ($key) {
	$computedValue = ...; // heavy computations
	return $computedValue;
});

如果未指定第二个参数 $value = $cache->load($key),如果项不在缓存中,则返回 null

非常好的是,可以缓存任何可序列化的结构,而不仅仅是字符串。同样也适用于键。

可以使用方法 remove() 从缓存中清除项

$cache->remove($key);

您还可以使用方法 $cache->save($key, $value, array $dependencies = []) 缓存项。然而,上述使用 load() 的方法更受欢迎。

记忆化

记忆化意味着缓存函数或方法的结果,以便下次可以直接使用它,而不是反复计算相同的事情。

可以使用 call(callable $callback, ...$args) 调用记忆化的方法和函数

$result = $cache->call('gethostbyaddr', $ip);

对于每个参数 $ip,函数 gethostbyaddr() 只调用一次,并且下次将返回缓存中的值。

也可以为方法或函数创建一个记忆化包装器,稍后可以调用它

function factorial($num)
{
	return ...;
}

$memoizedFactorial = $cache->wrap('factorial');

$result = $memoizedFactorial(5); // counts it
$result = $memoizedFactorial(5); // returns it from cache

过期 & 无效化

使用缓存时,必须解决这样一个问题:一些之前保存的数据将随着时间的推移而变得无效。Nette 框架提供了一个机制,如何限制数据的有效性以及如何以受控的方式删除它们(“使它们无效”,使用框架术语)。

数据的有效性是在使用方法的第三个参数保存时设置的,例如:

$cache->save($key, $value, [
	Cache::Expire => '20 minutes',
]);

或者使用通过引用传递给 load() 方法回调函数的 $dependencies 参数,例如

$value = $cache->load($key, function (&$dependencies) {
	$dependencies[Cache::Expire] = '20 minutes';
	return ...;
]);

或者使用 load() 方法的第三个参数,例如

$value = $cache->load($key, function () {
	return ...;
], [Cache::Expire => '20 minutes']);

在以下示例中,我们将假设第二种变体,即存在一个变量 $dependencies

过期时间

最简单的过期是时间限制。以下是缓存有效期为20分钟的方法

// it also accepts the number of seconds or the UNIX timestamp
$dependencies[Cache::Expire] = '20 minutes';

如果我们想通过每次读取来扩展有效期,可以通过这种方式实现,但请注意,这将增加缓存开销

$dependencies[Cache::Sliding] = true;

方便的选项是让数据在特定文件更改或几个文件中的任何一个更改时过期。例如,可以用于缓存处理这些文件产生的数据。使用绝对路径。

$dependencies[Cache::Files] = '/path/to/data.yaml';
// nebo
$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml'];

我们可以让缓存中的一个项在另一个项(或几个中的任何一个)过期时过期。当我们通过其他键缓存整个HTML页面及其片段时,可以使用此功能。一旦片段更改,整个页面就变为无效。如果我们有存储在键如 frag1frag2 下的片段,我们将使用

$dependencies[Cache::Items] = ['frag1', 'frag2'];

过期时间也可以通过自定义函数或静态方法来控制,这些方法始终在读取时决定项是否仍然有效。例如,我们可以让项在PHP版本更改时过期。我们将创建一个函数来比较当前版本与参数,并在保存时将一个数组以 [函数名, ...参数] 的形式添加到依赖项中

function checkPhpVersion($ver): bool
{
	return $ver === PHP_VERSION_ID;
}

$dependencies[Cache::Callbacks] = [
	['checkPhpVersion', PHP_VERSION_ID] // expire when checkPhpVersion(...) === false
];

当然,所有条件都可以组合。然后,当至少有一个条件不满足时,缓存将过期。

$dependencies[Cache::Expire] = '20 minutes';
$dependencies[Cache::Files] = '/path/to/data.yaml';

使用标签进行失效

标签是一个非常实用的失效工具。我们可以将一系列标签(任意字符串)分配给缓存中存储的每个项。例如,假设我们有一个包含文章和评论的HTML页面,我们想要缓存它。因此,我们在保存到缓存时指定标签

$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"];

现在,让我们转向管理。这里我们有一个文章编辑表单。在将文章保存到数据库的同时,我们调用 clean() 命令,这将根据标签删除缓存项

$cache->clean([
	Cache::Tags => ["article/$articleId"],
]);

同样,在添加新评论(或编辑评论)的地方,我们不会忘记使相关的标签失效

$cache->clean([
	Cache::Tags => ["comments/$articleId"],
]);

我们得到了什么?那就是我们的HTML缓存将在文章或评论更改时失效(删除)。在编辑ID为10的文章时,标签 article/10 将被强制失效,带有该标签的HTML页面将从缓存中删除。当在相关文章下插入新评论时,也会发生同样的事情。

标签需要 Journal

按优先级进行失效

我们可以设置缓存中单个项的优先级,然后在例如缓存超过一定大小时以受控的方式删除它们

$dependencies[Cache::Priority] = 50;

删除优先级等于或低于100的所有项

$cache->clean([
	Cache::Priority => 100,
]);

优先级需要所谓的 Journal

清除缓存

Cache::All 参数清除一切

$cache->clean([
	Cache::All => true,
]);

批量读取

对于批量读取和写入缓存,使用 bulkLoad() 方法,其中我们传递一个键数组并获取一个值数组

$values = $cache->bulkLoad($keys);

bulkLoad() 方法与 load() 类似,具有第二个回调参数,该参数传递生成的项的键

$values = $cache->bulkLoad($keys, function ($key, &$dependencies) {
	$computedValue = ...; // heavy computations
	return $computedValue;
});

输出缓存

输出可以被优雅地捕获和缓存

if ($capture = $cache->start($key)) {

	echo ... // printing some data

	$capture->end(); // save the output to the cache
}

如果输出已在缓存中存在,则 start() 方法打印它并返回 null,因此不会执行条件。否则,它开始缓冲输出,并返回 $capture 对象,我们可以使用该对象最终将数据保存到缓存中。

Latte 中的缓存

在模板中使用Latte缓存非常简单,只需用标签{cache}...{/cache}包裹模板的一部分。当源模板发生变化时(包括{cache}标签内的任何包含模板),缓存将自动失效。标签{cache}可以嵌套,当嵌套块失效(例如,由标签触发)时,父块也会失效。

在标签中可以指定缓存将绑定的键(此处为变量$id)以及设置过期时间和失效标签

{cache $id, expire => '20 minutes', tags => [tag1, tag2]}
	...
{/cache}

所有参数都是可选的,因此您不必指定过期时间、标签或键。

缓存的使用也可以通过if来条件化——只有当条件满足时,内容才会被缓存

{cache $id, if => !$form->isSubmitted()}
	{$form}
{/cache}

存储

存储是一个表示数据物理存储位置的对象。我们可以使用数据库、Memcached服务器或最易用的存储,即磁盘上的文件。

FileStorage

将缓存写入磁盘上的文件。存储Nette\Caching\Storages\FileStorage对性能进行了非常好的优化,并且首先确保了操作的全原子性。这意味着当使用缓存时,不会发生我们读取尚未由另一个线程完全写入的文件,或者有人会“在你的手中”删除它的情况。因此,使用缓存是完全安全的。

此存储还有一个重要的内置功能,可以防止在清除缓存或缓存冷(即未创建)时CPU使用率急剧增加。这是缓存雪崩的预防。有时候,会有多个并发请求需要从缓存中获取相同的内容(例如,昂贵SQL查询的结果),因为没有缓存,所有进程都会开始执行相同的SQL查询。处理器负载会加倍,甚至可能发生没有线程在时间限制内响应的情况,缓存没有创建,应用崩溃。幸运的是,Nette的缓存工作方式是这样的,当有多个并发请求对一个项目时,它只由第一个线程生成,其他线程等待,然后使用生成的结果。

创建FileStorage的示例

// the storage will be the directory '/path/to/temp' on the disk
$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp');

MemcachedStorage

Memcached服务器是一个高性能的分布式存储系统,其适配器为Nette\Caching\Storages\MemcachedStorage

需要PHP扩展memcached

$storage = new Nette\Caching\Storages\MemcachedStorage('10.0.0.158');

MemoryStorage

Nette\Caching\Storages\MemoryStorage是一个将数据存储在PHP数组中的存储,因此当请求结束时数据将丢失。

$storage = new Nette\Caching\Storages\MemoryStorage;

SQLiteStorage

SQLite数据库和适配器Nette\Caching\Storages\SQLiteStorage提供了一个在磁盘上的单个文件中缓存的方法。配置将指定此文件的路径。

需要PHP扩展pdopdo_sqlite

$storage = new Nette\Caching\Storages\SQLiteStorage('/path/to/cache.sdb');

DevNullStorage

存储的特殊实现是Nette\Caching\Storages\DevNullStorage,它实际上并不存储任何数据。因此,它适用于测试,如果我们想消除缓存的影响。

$storage = new Nette\Caching\Storages\DevNullStorage;

Journal

Nette将标签和优先级存储在所谓的日志中。默认情况下,SQLite和文件journal.s3db用于此目的,并且需要PHP扩展pdopdo_sqlite

如果你喜欢Nette,请现在捐赠。谢谢!