pbaszak/symfony-messenger-cache-bundle

Messenger Cache Bundle 是一个用于 Symfony Messenger 组件的包,它允许使用属性创建、使无效和刷新缓存。这可以加快异步任务的处理速度和效率。

1.9.0 2023-06-02 14:53 UTC

This package is auto-updated.

Last update: 2024-10-01 00:11:07 UTC


README

兼容性

  • PHP 8.0 - PHP 8.2
  • Symfony 5.4 - Symfony 6.2

安装

composer require pbaszak/symfony-messenger-cache-bundle

config/bundles.php 文件中

<?php

return [
    // ...
    PBaszak\MessengerCacheBundle\MessengerCacheBundle::class => ['all' => true],
];

快速入门

步骤 0

安装包后,首先确保您已在 config/packages/messenger.yaml 文件中定义了 default_bus。如果您没有定义它,Symfony 将返回错误或什么也不做,这也会成为问题,如果您没有将所有 MessageBusInterface $messageBus 注入装饰为 MessageBusCacheDecorator。在大多数情况下,它应该看起来像这样

framework:
    messenger:
        default_bus: messenger.bus.default
        buses:
            messenger.bus.default:

请注意,这里没有声明 cachedMessage.bus,它已经由该包声明,您可以通过将构造函数参数名称从 MessageBusInterface $messageBus 更改为 MessageBusInterface $cachedMessageBus 来使用它。

步骤 1

在首次编译 Symfony 时,可能会出现错误,表明您尚未定义默认的 cache pool,您可以在 messenger_cache.poolsframework.cache.pools 数组中定义它。此数组负责 MessengerCacheManager 将处理的缓存适配器的列表。要正确声明它,首先访问 config/packages/cache.yaml 文件,在那里您将找到 cache pools 的定义。在 framework.cache.pools 中的定义情况下,默认 pool 被命名为 app,在 messenger_cache.pools 中的别名定义情况下,默认 pool 被命名为 default。以下是从 config/packages/cache.yaml 的示例文件

framework:
    cache:
        pools:
            app: # By default, the pool used by the bundle is the one named `app`.
                adapter: cache.adapter.redis_tag_aware
                tags: true
            runtime: 
                adapter: cache.adapter.array
                tags: true
            filesystem:
                adapter: cache.adapter.filesystem

如果您不使用缓存使无效,则没有义务使用支持标签的适配器。但是,即使在这种情况下,我也建议使用支持缓存的适配器。您项目中没有 config/packages/messenger_cache.yaml 文件,并且您不需要它作为“快速入门”的一部分。但是,下面在这个 README 文件中您将找到有关此类文件应如何以及它有什么配置选项的信息。

步骤 2

根据以下示例修改您要缓存其响应的消息类。请注意,我选择了一个更复杂的示例来向您展示如何以这种方式将缓存与用户关联,以便可以使其无效,我认为这将是最常见的用例

# src/Application/User/Query/GetUserConfig.php
use PBaszak\MessengerCacheBundle\Attribute\Cache; # required attribute
use PBaszak\MessengerCacheBundle\Contract\Optional\DynamicTags; # optional interface
use PBaszak\MessengerCacheBundle\Contract\Required\Cacheable; # required interface

#[Cache(ttl: 3600)]
class GetUserConfig implements Cacheable, DynamicTags
{
    public function __construct(public readonly string $userId) {}

    public function getDynamicTags(): array
    {
        return ['user_' . $this->userId];
    }
}

步骤 3

修改执行 $this->messageBus->dispatch(new GetUserConfig($userId))$this->handle(new GetUserConfig($userId)) 的类的构造函数。

修改前

class UserConfigController extends AbstractController
{
    public function __construct(MessageBusInterface $messageBus) {}
}

修改后

class UserConfigController extends AbstractController
{
    public function __construct(MessageBusInterface $cachedMessageBus) {}
}

完成.
现在,如果在 UserConfigController 类中调用 GetUserConfig(),则响应将在默认缓存池中缓存。

额外步骤(使无效)

# src/Application/User/Command/UpdateUserConfig.php
use PBaszak\MessengerCacheBundle\Attribute\Invalidate; # required attribute
use PBaszak\MessengerCacheBundle\Contract\Optional\DynamicTags; # optional interface
use PBaszak\MessengerCacheBundle\Contract\Required\CacheInvalidation; # required interface

#[Invalidate()]
class UpdatetUserConfig implements CacheInvalidation, DynamicTags
{
    public function __construct(
        public readonly string $usesId,
        public readonly array $config,
    ) {}

    public function getDynamicTags(): array
    {
        return ['user_' . $this->userId];
    }
}

如您所见,getDynamicTags 方法没有改变,这就是我强烈建议将此方法放在 Traits 中的原因。

额外步骤(删除用户上下文,例如在用户组缓存的情况下)

# src/Application/User/Query/GetUserConfig.php
use PBaszak\MessengerCacheBundle\Attribute\Cache; # required attribute
use PBaszak\MessengerCacheBundle\Contract\Optional\HashableInstance; # optional interface
use PBaszak\MessengerCacheBundle\Contract\Required\Cacheable; # required interface

#[Cache(refreshAfter: 3600)] # The "refreshAfter" will cause that after an hour, on the next cache call, it will be asynchronously refreshed and the old cache will be returned.
class GetCompanyConfig implements Cacheable, HashableInstance
{
    public function __construct(
        public readonly ?User $user,
        public readonly string $companyId,
    ) {}

    public function getHashableInstance(): Cacheable
    {
        return new self(null, $this->companyId); # The original Message will still be processed, but it will be used to create a unique hash. Therefore, if we didn't remove the user context, the cache would only be available to them, not the entire company.
    }
}

如果我不能删除用户上下文,例如因为我不想用 20 个构造函数参数写入新的 self(),我为您提供了一个替代方案!

# src/Application/User/Query/GetUserConfig.php
use PBaszak\MessengerCacheBundle\Attribute\Cache; # required attribute
use PBaszak\MessengerCacheBundle\Contract\Optional\UniqueHash; # optional interface
use PBaszak\MessengerCacheBundle\Contract\Required\Cacheable; # required interface

#[Cache(refreshAfter: 3600)]
class GetCompanyConfig implements Cacheable, UniqueHash
{
    public function __construct(
        public readonly ?User $user,
        public readonly string $companyId,
    ) {}

    public function getUniqueHash(): string
    {
        return 'company_' . $this->companyId;
    }
}

这够了吗?缓存会与具有相同 UniqueHash 的另一个 Message 的缓存冲突吗?
不会。 缓存键还包括从 Message 的完整类名生成的哈希。


快速入门到此结束。您已准备好在应用程序中部署高性能缓存,简单的实现允许创建真正的先进缓存和缓存失效系统,这正是这个包被创建的目的。

配置

创建或复制文件 messenger_cache.yaml

# copy
cp vendor/pbaszak/messenger-cache-bundle/config/packages/messenger_cache.yaml config/packages/messenger_cache.yaml
#
# or
#
# create
touch config/packages/messenger_cache.yaml
# config/packages/messenger_cache.yaml
messenger_cache:
    # If You use RefreshAsync (it's message) / refreshAfter (in Cache attribute argument) then
    # you must have declared time of live info that refresh was triggered. It's deleted
    # after succesful refresh. To short value may trigger more than one refresh action for 
    # specific item. Recommended is 10 minutes.
    refresh_triggered_ttl: 600
    # If You need handle cache events like hit, miss, refresh and stamps are not enough for
    # You then change this option to `true`. It add additional events to cache, but it costs performance
    use_events: true
    # aliases for pools to use them in cache attribute and cache invalidation attribute
    # aliases are required and `default` alias is required.
    pools:
        default: filesystem
        runtime: runtime
        redis: redis
    # this is default value and You don't need to add it, but if You want decorates different buses
    # you can declare them all here.
    decorated_message_buses:
        - cachedMessage.bus
    # message bus decorators whic You want add. The main cache decorator has 
    # priority 0 if higher it will be closer to main message bus.
    message_bus_decorators:
        '-1': PBaszak\MessengerCacheBundle\Tests\Helper\Domain\Decorator\LoggingMessageBusDecorator # this one decorator was only for tests. If You want logging decorator You have to make it, but before You start check `use_events`, maybe it will be better option to logging or metrics or anything You want like adding cache result header in response. Just make Your own EventListener ;).

配置描述

作为服务的池的示例声明

# config/cache.yaml
framework:
    cache:
        default_redis_provider: 'redis://redis:6379'
        pools:
            runtime: 
                adapter: cache.adapter.array
                tags: true
            filesystem: 
                adapter: cache.adapter.filesystem
            redis:
                adapter: cache.adapter.redis_tag_aware
                tags: true

注意:您的代码可能(很可能)会有所不同。

还有两个额外的命令

  • PBaszak\MessengerCacheBundle\Message\InvalidateAsync,
  • PBaszak\MessengerCacheBundle\Message\RefreshAsync,

您需要自己添加它们到异步处理中。如果您不这样做,它们将以同步方式执行,这会影响您从缓存中检索数据时应用程序的性能。

用法

示例 1(缓存)

一个由 Symfony Messenger 处理的示例类,它有一个自己的处理器,总是返回一个随机字符串。

# src/Application/Query/GetRandomString.php
use PBaszak\MessengerCacheBundle\Attribute\Cache;
use PBaszak\MessengerCacheBundle\Contract\Required\Cacheable;

#[Cache()]
class GetRandomString implements Cacheable
{
}

任何调用我们感兴趣的 Message 并返回其响应的管理器

# src/Domain/Manager/StringsManager.php
use App\Application\Query\GetRandomString;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\HandleTrait;

class StringsManager
{
    use HandleTrait;

    public function __construct(MessageBusInterface $cachedMessageBus)
    {
        $this->messageBus = $cachedMessageBus;
    }

    public function getAlwaysSameRandomString(): string
    {
        return $this->handle(
            new GetRandomString()
        );
    }
}

$stringsManager = new StringsManager();
$result0 = $stringsManager->getAlwaysSameRandomString();
$result1 = $stringsManager->getAlwaysSameRandomString();

var_dump($result0 === $result1); // true

示例 2(缓存失效)

# src/Application/Query/GetRandomString.php
use PBaszak\MessengerCacheBundle\Attribute\Cache;
use PBaszak\MessengerCacheBundle\Contract\Required\Cacheable;

#[Cache(tags: ['string_tag'])]
class GetRandomString implements Cacheable
{
}
# src/Application/Command/DoSth.php
use PBaszak\MessengerCacheBundle\Attribute\CacheInvalidation;
use PBaszak\MessengerCacheBundle\Contract\Required\CacheInvalidation;

#[Invalidate(['string_tag'])]
class DoSth implements CacheInvalidation
{
}
# src/Domain/Manager/StringsManager.php
use App\Application\Command\DoSth;
use App\Application\Query\GetRandomString;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\HandleTrait;

class StringsManager
{
    use HandleTrait;

    public function __construct(MessageBusInterface $cachedMessageBus)
    {
        $this->messageBus = $cachedMessageBus;
    }

    public function getAlwaysSameRandomString(): string
    {
        return $this->handle(
            new GetRandomString()
        );
    }

    public function doSth(): void
    {
        $this->handle(
            new DoSth()
        );
    }
}

$stringsManager = new StringsManager();
$result0 = $stringsManager->getAlwaysSameRandomString();
$stringsManager->doSth();
$result1 = $stringsManager->getAlwaysSameRandomString();

var_dump($result0 === $result1); // false

示例 3(强制缓存刷新戳)

添加装饰器

# config/packages/messenger_cache.yaml
message_bus_decorators:
    "-1": App\Infrastructure\Symfony\Messenger\MessageBusDecorator

装饰器示例实现

# src/Infrastructure/Symfony/Messenger/MessageBusDecorator.php
use PBaszak\MessengerCacheBundle\Stamps\ForceCacheRefreshStamp;

class MessageBusDecorator implements MessageBusInterface
{
    private Request $request;
    private static array $cacheRefreshedMessages;

    public function __construct(
        private MessageBusInterface $decorated,
        RequestStack $requestStack,
    ) {
        $this->request = $requestStack->getMainRequest();
        $this->forceCacheRefresh = $requestStack->getMainRequest()->query->has('forceCacheRefresh');
    }

    /** @param StampInterface[] $stamps */
    public function dispatch(object $message, array $stamps = []): Envelope
    {
        if (!in_array($message, self::$cacheRefreshedMessages) && $this->request->query->has('forceCacheRefresh')) {
            $stamps = array_merge($stamps, [new ForceCacheRefreshStamp()]);
            self::$cacheRefreshedMessages = $message;
        }

        return $this->decorated->dispatch($message, $stamps);
    }
}

此请求中传入的参数将导致请求中所有缓存的同步刷新。

curl --location --request GET 'https:///strings/random?forceCacheRefresh'

为了更精确的解决方案,我建议创建自己的方法来决定刷新哪个缓存,而不是在单个请求中刷新所有可能的缓存。我个人使用一个条件函数 if ((new ReflectionClass($message))->getShortName() === $this->request->query->get('forceCacheRefresh')) { // add ForceCacheRefreshStamp }。然后示例参数可能看起来像这样:?forceCacheRefresh=GetUserConfig。

# src/Domain/Manager/StringsManager.php
use App\Application\Query\GetRandomString;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\HandleTrait;

class StringsManager
{
    use HandleTrait;

    public function __construct(MessageBusInterface $cachedMessageBus)
    {
        $this->messageBus = $cachedMessageBus;
    }

    public function getAlwaysSameRandomString(): string
    {
        return $this->handle(
            new GetRandomString()
        );
    }
}

$stringsManager = new StringsManager();
$result0 = $stringsManager->getAlwaysSameRandomString();
$result1 = $stringsManager->getAlwaysSameRandomString();

var_dump($result0 === $result1); // false

详细设置

属性

缓存

失效

可选接口