缓存
使用缓存是使您的应用程序运行更快的绝佳方式。Symfony 缓存组件附带了许多适用于不同存储的适配器。每个适配器都为了高性能而开发。
以下示例展示了缓存的典型用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
use Symfony\Contracts\Cache\ItemInterface;
// The callable will only be executed on a cache miss.
$value = $pool->get('my_cache_key', function (ItemInterface $item): string {
$item->expiresAfter(3600);
// ... do some HTTP request or heavy computations
$computedValue = 'foobar';
return $computedValue;
});
echo $value; // 'foobar'
// ... and to remove the cache key
$pool->delete('my_cache_key');
Symfony 支持缓存契约和 PSR-6/16 接口。您可以在组件文档中阅读更多相关信息。
使用 FrameworkBundle 配置缓存
在配置缓存组件时,您应该了解以下几个概念
- 缓存池
- 这是一个您将与之交互的服务。每个缓存池都将始终拥有自己的命名空间和缓存项。缓存池之间永远不会发生冲突。
- 适配器
- 适配器是一个用于创建缓存池的模板。
- 提供商
- 提供商是某些适配器用于连接到存储的服务。Redis 和 Memcached 是此类适配器的示例。如果 DSN 用作提供商,则会自动创建服务。
默认情况下,始终启用两个缓存池。它们是 cache.app
和 cache.system
。系统缓存用于诸如注解、序列化器和验证之类的内容。cache.app
可在您的代码中使用。您可以使用 app
和 system
键来配置它们使用的适配器(模板),例如
1 2 3 4 5
# config/packages/cache.yaml
framework:
cache:
app: cache.adapter.filesystem
system: cache.adapter.system
提示
虽然可以重新配置 system
缓存,但建议保留 Symfony 应用于它的默认配置。
缓存组件预配置了一系列适配器
- cache.adapter.apcu
- cache.adapter.array
- cache.adapter.doctrine_dbal
- cache.adapter.filesystem
- cache.adapter.memcached
- cache.adapter.pdo
- cache.adapter.psr6
- cache.adapter.redis
- cache.adapter.redis_tag_aware (Redis 适配器,针对标签优化)
注意
还有一个特殊的 cache.adapter.system
适配器。建议将其用于系统缓存。此适配器使用一些逻辑来根据您的系统(PHP 文件或 APCu)动态选择最佳可能的存储。
其中一些适配器可以通过快捷方式进行配置。
1 2 3 4 5 6 7 8 9 10
# config/packages/cache.yaml
framework:
cache:
directory: '%kernel.cache_dir%/pools' # Only used with cache.adapter.filesystem
default_doctrine_dbal_provider: 'doctrine.dbal.default_connection'
default_psr6_provider: 'app.my_psr6_service'
default_redis_provider: 'redis://127.0.0.1'
default_memcached_provider: 'memcached://127.0.0.1'
default_pdo_provider: 'pgsql:host=localhost'
7.1
在 Symfony 7.1 中引入了使用 DSN 作为 PDO 适配器的提供商。
创建自定义(命名空间)缓存池
您还可以创建更自定义的缓存池
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
# config/packages/cache.yaml
framework:
cache:
default_memcached_provider: 'memcached://127.0.0.1'
pools:
# creates a "custom_thing.cache" service
# autowireable via "CacheInterface $customThingCache"
# uses the "app" cache configuration
custom_thing.cache:
adapter: cache.app
# creates a "my_cache_pool" service
# autowireable via "CacheInterface $myCachePool"
my_cache_pool:
adapter: cache.adapter.filesystem
# uses the default_memcached_provider from above
acme.cache:
adapter: cache.adapter.memcached
# control adapter's configuration
foobar.cache:
adapter: cache.adapter.memcached
provider: 'memcached://user:[email protected]'
# uses the "foobar.cache" pool as its backend but controls
# the lifetime and (like all pools) has a separate cache namespace
short_cache:
adapter: foobar.cache
default_lifetime: 60
每个缓存池管理一组独立的缓存键:来自不同缓存池的键永远不会冲突,即使它们共享相同的后端。这是通过使用命名空间前缀键来实现的,该命名空间通过哈希缓存池的名称、缓存适配器类的名称和一个可配置种子生成,该种子默认为项目目录和编译后的容器类。
每个自定义缓存池都变成一个服务,其服务 ID 是缓存池的名称(例如 custom_thing.cache
)。还为每个缓存池创建了一个自动装配别名,使用其名称的驼峰式版本 - 例如,通过命名参数 $customThingCache
并使用 CacheInterface 或 Psr\Cache\CacheItemPoolInterface
进行类型提示,可以自动注入 custom_thing.cache
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
use Symfony\Contracts\Cache\CacheInterface;
// ...
// from a controller method
public function listProducts(CacheInterface $customThingCache): Response
{
// ...
}
// in a service
public function __construct(private CacheInterface $customThingCache)
{
// ...
}
提示
如果您需要命名空间与第三方应用程序互操作,您可以通过设置 cache.pool
服务标签的 namespace
属性来控制自动生成。例如,您可以覆盖适配器的服务定义
1 2 3 4 5 6 7 8
# config/services.yaml
services:
# ...
app.cache.adapter.redis:
parent: 'cache.adapter.redis'
tags:
- { name: 'cache.pool', namespace: 'my_custom_namespace' }
自定义提供商选项
某些提供商具有可以配置的特定选项。RedisAdapter 允许您创建具有选项 timeout
、retry_interval
等的提供商。要使用具有非默认值的这些选项,您需要创建自己的 \Redis
提供商并在配置缓存池时使用它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# config/packages/cache.yaml
framework:
cache:
pools:
cache.my_redis:
adapter: cache.adapter.redis
provider: app.my_custom_redis_provider
services:
app.my_custom_redis_provider:
class: \Redis
factory: ['Symfony\Component\Cache\Adapter\RedisAdapter', 'createConnection']
arguments:
- 'redis://127.0.0.1'
- { retry_interval: 2, timeout: 10 }
创建缓存链
不同的缓存适配器有不同的优点和缺点。有些可能非常快,但优化用于存储小项目,有些可能能够包含大量数据,但速度很慢。为了兼得两者之长,您可以使用适配器链。
缓存链将多个缓存池组合成一个。当在缓存链中存储项目时,Symfony 会按顺序将其存储在所有缓存池中。当检索项目时,Symfony 尝试从第一个缓存池中获取它。如果未找到,它会尝试下一个缓存池,直到找到该项目或抛出异常。由于这种行为,建议按从快到慢的顺序定义链中的适配器。
如果在将项目存储在缓存池中时发生错误,Symfony 会将其存储在其他缓存池中,并且不会抛出异常。稍后,当检索项目时,Symfony 会自动将该项目存储在所有缺失的缓存池中。
1 2 3 4 5 6 7 8 9 10
# config/packages/cache.yaml
framework:
cache:
pools:
my_cache_pool:
default_lifetime: 31536000 # One year
adapters:
- cache.adapter.array
- cache.adapter.apcu
- {name: cache.adapter.redis, provider: 'redis://user:[email protected]'}
使用缓存标签
在具有许多缓存键的应用程序中,组织存储的数据可能很有用,以便能够更有效地使缓存失效。实现这一目标的一种方法是使用缓存标签。可以向缓存项添加一个或多个标签。可以使用一个函数调用使具有相同标签的所有项目失效
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
class SomeClass
{
// using autowiring to inject the cache pool
public function __construct(
private TagAwareCacheInterface $myCachePool,
) {
}
public function someMethod(): void
{
$value0 = $this->myCachePool->get('item_0', function (ItemInterface $item): string {
$item->tag(['foo', 'bar']);
return 'debug';
});
$value1 = $this->myCachePool->get('item_1', function (ItemInterface $item): string {
$item->tag('foo');
return 'debug';
});
// Remove all cache keys tagged with "bar"
$this->myCachePool->invalidateTags(['bar']);
}
}
缓存适配器需要实现 TagAwareCacheInterface 以启用此功能。这可以通过使用以下配置来添加。
1 2 3 4 5 6
# config/packages/cache.yaml
framework:
cache:
pools:
my_cache_pool:
adapter: cache.adapter.redis_tag_aware
默认情况下,标签存储在同一个缓存池中。这在大多数情况下都很好。但有时最好将标签存储在不同的缓存池中。这可以通过指定适配器来实现。
1 2 3 4 5 6 7 8 9
# config/packages/cache.yaml
framework:
cache:
pools:
my_cache_pool:
adapter: cache.adapter.redis
tags: tag_pool
tag_pool:
adapter: cache.adapter.apcu
注意
接口 TagAwareCacheInterface 自动装配到 cache.app
服务。
清除缓存
要清除缓存,您可以使用 bin/console cache:pool:clear [pool]
命令。这将从您的存储中删除所有条目,您将必须重新计算所有值。您还可以将您的缓存池分组为“缓存清除器”。默认情况下有 3 个缓存清除器
cache.global_clearer
cache.system_clearer
cache.app_clearer
全局清除器清除每个缓存池中的所有缓存项。系统缓存清除器在 bin/console cache:clear
命令中使用。应用清除器是默认的清除器。
查看所有可用的缓存池
1
$ php bin/console cache:pool:list
清除一个缓存池
1
$ php bin/console cache:pool:clear my_cache_pool
清除所有自定义缓存池
1
$ php bin/console cache:pool:clear cache.app_clearer
清除所有缓存池
1
$ php bin/console cache:pool:clear --all
清除除某些以外的所有缓存池
1
$ php bin/console cache:pool:clear --all --exclude=my_cache_pool --exclude=another_cache_pool
清除所有地方的缓存
1
$ php bin/console cache:pool:clear cache.global_clearer
按标签清除缓存
1 2 3 4 5 6 7 8 9 10 11
# invalidate tag1 from all taggable pools
$ php bin/console cache:pool:invalidate-tags tag1
# invalidate tag1 & tag2 from all taggable pools
$ php bin/console cache:pool:invalidate-tags tag1 tag2
# invalidate tag1 & tag2 from cache.app pool
$ php bin/console cache:pool:invalidate-tags tag1 tag2 --pool=cache.app
# invalidate tag1 & tag2 from cache1 & cache2 pools
$ php bin/console cache:pool:invalidate-tags tag1 tag2 -p cache1 -p cache2
加密缓存
要使用 libsodium
加密缓存,您可以使用 SodiumMarshaller。
首先,您需要生成一个安全密钥并将其作为 CACHE_DECRYPTION_KEY
添加到您的密钥存储中
1
$ php -r 'echo base64_encode(sodium_crypto_box_keypair());'
然后,使用此密钥注册 SodiumMarshaller
服务
1 2 3 4 5 6 7 8 9 10 11
# config/packages/cache.yaml
# ...
services:
Symfony\Component\Cache\Marshaller\SodiumMarshaller:
decorates: cache.default_marshaller
arguments:
- ['%env(base64:CACHE_DECRYPTION_KEY)%']
# use multiple keys in order to rotate them
#- ['%env(base64:CACHE_DECRYPTION_KEY)%', '%env(base64:OLD_CACHE_DECRYPTION_KEY)%']
- '@.inner'
危险
这将加密缓存项的值,但不会加密缓存键。请注意不要在键中泄露敏感数据。
配置多个密钥时,第一个密钥将用于读取和写入,而其他密钥仅用于读取。一旦使用旧密钥加密的所有缓存项都已过期,您可以完全删除 OLD_CACHE_DECRYPTION_KEY
。
异步计算缓存值
缓存组件使用概率性提前过期算法来防止缓存雪崩问题。这意味着一些缓存项在仍然新鲜时会被选为提前过期。
默认情况下,过期的缓存项是同步计算的。但是,您可以通过使用Messenger 组件将值计算委托给后台 worker 来异步计算它们。在这种情况下,当查询一个项时,会立即返回其缓存值,并且通过 Messenger 总线分发 EarlyExpirationMessage。
当此消息由消息消费者处理时,刷新的缓存值是异步计算的。下次查询该项时,刷新的值将是新鲜的并被返回。
首先,创建一个将计算项目值的服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// src/Cache/CacheComputation.php
namespace App\Cache;
use Symfony\Contracts\Cache\ItemInterface;
class CacheComputation
{
public function compute(ItemInterface $item): string
{
$item->expiresAfter(5);
// this is just a random example; here you must do your own calculation
return sprintf('#%06X', mt_rand(0, 0xFFFFFF));
}
}
此缓存值将从控制器、另一个服务等请求。在以下示例中,该值是从控制器请求的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// src/Controller/CacheController.php
namespace App\Controller;
use App\Cache\CacheComputation;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
class CacheController extends AbstractController
{
#[Route('/cache', name: 'cache')]
public function index(CacheInterface $asyncCache): Response
{
// pass to the cache the service method that refreshes the item
$cachedValue = $asyncCache->get('my_value', [CacheComputation::class, 'compute'])
// ...
}
}
最后,配置一个新的缓存池(例如,名为 async.cache
),它将使用消息总线在 worker 中计算值
1 2 3 4 5 6 7 8 9 10 11 12
# config/packages/framework.yaml
framework:
cache:
pools:
async.cache:
early_expiration_message_bus: messenger.default_bus
messenger:
transports:
async_bus: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
'Symfony\Component\Cache\Messenger\EarlyExpirationMessage': async_bus
您现在可以启动消费者
1
$ php bin/console messenger:consume async_bus
就是这样!现在,每当从此缓存池查询一个项目时,都会立即返回其缓存值。如果它被选为提前过期,则会通过总线发送一条消息,以安排后台计算来刷新该值。