跳到内容

缓存

编辑此页

使用缓存是使您的应用程序运行更快的绝佳方式。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.appcache.system。系统缓存用于诸如注解、序列化器和验证之类的内容。cache.app 可在您的代码中使用。您可以使用 appsystem 键来配置它们使用的适配器(模板),例如

1
2
3
4
5
# config/packages/cache.yaml
framework:
    cache:
        app: cache.adapter.filesystem
        system: cache.adapter.system

提示

虽然可以重新配置 system 缓存,但建议保留 Symfony 应用于它的默认配置。

缓存组件预配置了一系列适配器

注意

还有一个特殊的 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 并使用 CacheInterfacePsr\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 允许您创建具有选项 timeoutretry_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

就是这样!现在,每当从此缓存池查询一个项目时,都会立即返回其缓存值。如果它被选为提前过期,则会通过总线发送一条消息,以安排后台计算来刷新该值。

这项工作,包括代码示例,根据 Creative Commons BY-SA 3.0 许可获得许可。
目录
    版本