studocu/cacheable-entities

可缓存实体是一个抽象层,用于提取与缓存相关的责任。

v1.0.0 2024-02-09 12:35 UTC

This package is not auto-updated.

Last update: 2024-09-20 15:31:10 UTC


README

可缓存实体

Tests Codecov

可缓存实体是一个具有观点的基础设施,充当抽象层以提取与缓存相关的责任。

class RecommendedBooksQuery implements Cacheable, SerializableCacheable
{
    public function getCacheTTL(): int
    {
        return 3600 * 24;
    }

    public function getCacheKey(): string
    {
        return "books:popular.v1";
    }

    public function get(): Collection
    {
        return Book::query()
            ->wherePopular()
            ->orderByDesc('document_popularity_scores.score')
            ->take(config('popular.books.limit'))
            ->get();
    }
    
    public function serialize(mixed $value): array
    {
        return $value->pluck('id')->all();
    }
    
    public function unserialize(mixed $value): Collection
    {
        return Book::query()->findMany($value);
    }
}

$query = new RecommendedBooksQuery();

// Get a non-blocking cache result in the web endpoint.
resolve(AsyncCache::class)->get($query);

// Get a blocking cache result in the API endpoint.
resolve(SyncCache::class)->get($query);

// Get real time value
$query->get();

功能

  • 封装键和TLL管理。
  • 阻塞/非阻塞缓存策略。
  • 轻松序列化/反序列化缓存值。
  • 自定义缓存未命中解决。
  • 直接访问实时值(不通过缓存)。

目录

安装

您可以通过composer安装此包

composer require studocu/cacheable-entities

背景

在Studocu,我们处理大量数据和请求。在成长过程中,我们发现自己在缓存键和缓存中到处都是。因此,为了在整个代码库中统一缓存方法,我们制定了一个内部标准化的(或称具有观点的)基础设施,称为“可缓存实体”。这个基础设施充当抽象层,提取与缓存相关的责任。我们将这个基础设施隔离成一个独立的Laravel包,并使其开源。

了解更多关于背景的信息,请参阅 <可缓存实体:具有故事的Laravel包>。

使用

定义可缓存实体

要使一个类成为可缓存实体,它必须实现StuDocu\CacheableEntities\Contracts\Cacheable契约。

接口实现需要定义以下方法

  • getCacheTTL:返回缓存的TTL(以秒为单位)。
  • getCacheKey:返回缓存键。
  • get:计算实体值。

访问可缓存实体

缓存策略

在某些情况下,您可能需要将相同的实体以不同的方式缓存/访问;要么是阻塞的,要么是非阻塞的。

  • 阻塞缓存(同步):如果没有值,我们计算它,缓存它,并立即提供结果。
  • 非阻塞缓存(异步):如果没有值,我们将派遣一个作业来计算它,并返回一个空状态(例如null,空集合或空数组)。

访问

通过缓存

要使用上述两种缓存策略中的任何一种,我们都可以访问两个可用的实用工具类:SyncCacheAsyncCache

  • StuDocu\CacheableEntities\SyncCache@get:接受一个可缓存实体,如果尚未预先缓存,则将等待并缓存结果。
  • StuDocu\CacheableEntities\AsyncCache@get:接受一个可缓存实体,如果尚未预先缓存,则将派遣一个作业来计算实体值,然后返回一个空状态。否则,它将返回缓存的值。

⚠️ 重要:如果您有多个服务器基础设施,并且计划异步使用可缓存的实体,请确保首先单独创建和部署该实体,而不使用 asyncCache。否则,在部署时可能会出现类(作业)反序列化错误。某些区域可能会在其他区域之前部署。

示例

<?php

use Author;
use Book;
use Illuminate\Database\Eloquent\Collection;
use StuDocu\CacheableEntities\Contracts\Cacheable;
use StuDocu\CacheableEntities\Contracts\SerializableCacheable;

class AuthorPopularBooksQuery implements Cacheable
{
    public const DEFAULT_LIMIT = 8;

    public function __construct(
        protected readonly Author $author,
        protected readonly int $limit = self::DEFAULT_LIMIT,
    ) {
    }

    public function getCacheTTL(): int
    {
        return 3600 * 24;
    }

    public function getCacheKey(): string
    {
        return "authors:{$this->author->id}:books:popular.v1";
    }

    public function get(): Collection
    {
        return Book::query()
            ->join('book_popularity_scores', 'book_popularity_scores.book_id', '=', 'books.id')
            ->where('author_id', $this->author->id)
            ->whereValid()
            ->whereHas('ratings')
            ->orderByDesc('document_popularity_scores.score')
            ->take($this->limit)
            ->get()
            ->each
            ->setRelation('author', $this->author);
    }
}

// Usage

// ...

$query = new AuthorPopularBooksQuery($author);

// Get a non-blocking cache result in the web endpoint.
resolve(AsyncCache::class)->get($query);

// Get a blocking cache result in the API endpoint.
resolve(SyncCache::class)->get($query);
不使用缓存

要获取实体的新值,您可以在任何可缓存实体的实例上简单调用 @get。这将计算值而不会与缓存交互。

序列化/反序列化

在某些情况下,您可能不想缓存实际值,而是缓存值的元数据,例如,一个ID数组。稍后,当您访问缓存时,您会运行一个查询来根据ID查找记录。

为了使可缓存实体可序列化,它必须实现以下合约 StuDocu\CacheableEntities\Contracts\SerializableCacheable

接口实现需要定义以下方法

  • serialize(mixed $value): mixed:准备缓存结果。每当可缓存实体即将被缓存时,都会调用此方法。此方法的输出将是缓存值。
  • unserialize(mixed $value): mixed:恢复缓存的值的原始状态。每当读取缓存值时,都会调用此方法。此方法的输出将作为缓存值返回。

示例

<?php

// [...]
use StuDocu\CacheableEntities\Contracts\SerializableCacheable;

class AuthorPopularBooksQuery implements Cacheable, SerializableCacheable
{
   // [...]
   
   /**
    * @param Collection<Book> $value
    * @return array<int>
    */
   public function serialize(mixed $value): array
   {
       // `$value` represents the computed value of this query; it will be what we will get when calling self::get().
       return $value->pluck('id')->all();
   }
   
    /**
     * @param  int[]  $value
     * @return  Collection<int, Book>
     */
    public function unserialize(mixed $value): Collection
    {
        // `$value` represents what we've already cached previously, it will the result of self self::serialize(...)
        
        $booksFastAccess = array_flip($value);
        
        $books = Book::query()
            ->findMany($value)
            ->sortBy(fn (Book $book) => $booksFastAccess[$book->id] ?? 999)
            ->values();
    
        $this->setRelations($books);
    
        return $books;
    }
    
    /**
     * @param  ReturnStructure  $books
     */
    private function setRelations(Collection $books): void
    {
        $books->each->setRelation('author', $this->author);

        // Generally speaking, you can do eager loading and such in a similar fashion (for ::get and ::unserialize).
    }
}

// Usage is still unchanged.
$query = new AuthorPopularBooksQuery($author);

// Get a non-blocking cache result in the web endpoint.
resolve(\StuDocu\CacheableEntities\AsyncCache::class)->get($query);

// Get a blocking cache result in the API endpoint.
resolve(\StuDocu\CacheableEntities\SyncCache::class)->get($query);

损坏的序列化缓存值

在某些情况下,您可能会在反序列化时遇到无效的缓存值。

可能是以下情况

  • 无效的格式
  • 数据的元数据不再有效或不存在
  • 等。

当这种情况发生时,销毁该缓存值并重新开始是有益的。可缓存实体基础设施正是考虑到这一点而构建的,要指示它该值已损坏,需要(1)忘记它并(2)计算一个新值,您需要在您的 @unserialize 方法中抛出以下异常 \StuDocu\CacheableEntities\Exceptions\CorruptSerializedCacheValueException

示例

    /**
     * @return ReturnStructure
     *
     * @throws CorruptSerializedCacheValueException
     */
    public function unserialize(mixed $value, mixed $default = null): Collection
    {
        // Corrupt format cached
        if (! is_array($value)) {
            throw new CorruptSerializedCacheValueException();
        }

        if (empty($value)) {
            return Collection::empty();
        }

        $books = Book::query()->findMany($value);

        $this->eagerLoadRelations($books);

        return $books;
    }

反序列化时的注意事项

根据您如何序列化您的模型,您可能会在反序列化时丢失原始顺序,例如,当只缓存ID时。对于顺序很重要的实体,请确保在反序列化时保留原始顺序。

这里有几种实现方式

// Retaining the original order with array_search
$books = Book::query()
    ->findMany($value)
    ->sortBy(fn (Book $book) => array_search($book->id, $value))
    ->values();

// Retaining the original order with array_flip.
// A faster alternative than the above, using direct array access instead of `array_search`.`
$booksFastAccess = array_flip($value);
$book = Book::query()
    ->findMany($value)
    ->get()
    ->sortBy(fn (Book $book) => $booksFastAccess[$book->id] ?? 999)
    ->values();

// Retaining the original order with SQL.
$books = Book::query()
    ->orderByRaw(DB::raw('FIELD(id, ' . implode(',', $value) . ')'))
    ->get();

清除缓存

要使可缓存实体的缓存值无效,您需要使用 SyncCache::forget 方法。

使用 SyncCache 进行此操作的原因是无效化是即时发生的。

以下是一些示例

<?php
$query = new AuthorPopularBooks($author);
// Invalidate the cache (for example, in an event listener).
resolve(\StuDocu\CacheableEntities\SyncCache::class)->forget($query);

异步缓存默认值

当使用 AsyncCache 工具时,如果缓存了值,它将返回 null。在某些情况下,您可能需要更改默认值。

您需要做的只是使可缓存实体实现以下接口 StuDocu\CacheableEntities\Contracts\SupportDefaultCacheValue

接口实现需要定义以下方法

  • getCacheMissValue:当实体尚未缓存时返回默认值。
<?php

use Illuminate\Database\Eloquent\Collection;
use StuDocu\CacheableEntities\Contracts\SupportsDefaultValue;

class AuthorPopularBooks implements Cacheable, SupportsDefaultValue
{
   public function getCacheMissValue(): Collection
   {
      return Collection::empty();
   }
}

自缓存实体

通常,为了同步或异步访问缓存,我们需要中间实用程序类。但是,可能需要为了方便而隐藏这些细节。

在这种情况下,我们可以使用自缓存实体概念。

为了使实体可自缓存,它必须使用以下关注点 StuDocu\CacheableEntities\Concerns\SelfCacheable

具有该特质的任何类都可以访问以下方法

  • getCached:同步返回缓存值。
  • getCachedAsync:异步返回缓存值(如果尚未缓存,将调度作业)。
  • forgetCache:清除缓存值。

示例

// [...]
use StuDocu\CacheableEntities\Concerns\SelfCacheable;

class AuthorPopularBooksQuery implements Cacheable, SerializableCacheable
{
   use SelfCacheable;
   
   // [...]
}

$query = new AuthorPopularBooksQuery($author);

// Get a non-blocking cache result in the web endpoint.
$query->getCachedAsync();

// Get a blocking cache result in the API endpoint.
$query->getCached();

// Forget the cached value.
$query->forgetCache();

通用注解

为了确保可缓存实体在类型方面的安全使用,实现中提供了泛型模板。每次定义缓存实体时,都必须指定其泛型,除非您没有静态分析。

以下是一个指定所有合约和关注的泛型的示例。

/**
 * @phpstan-type ReturnStructure Collection<User>
 * @implements Cacheable<ReturnStructure>
 * @implements SerializableCacheable<ReturnStructure, string>
 * @implements SupportsDefaultValue<ReturnStructure>
 */
class CourseQuery implements Cacheable, SerializableCacheable, SupportsDefaultValue
{
    /** @phpstan-use SelfCacheable<ReturnStructure> */
    use SelfCacheable;
}

可缓存通用

此合约接受一个泛型定义 <TReturn>,这是在调用 get 计算其值时实体将返回的类型。

SerializableCacheable

此合约接受两个泛型定义 <TUnserialized, TSerialized>

  • TUnserialized:在反序列化缓存值时返回的类型。它应该与 TReturn 具有相同的形状以确保一致性。
  • TSerialized:在序列化结果时返回的类型。

支持默认值

此合约接受一个泛型定义 <TDefault>,这是在使用 AsyncCache 工具时缺失缓存时实体将返回的类型。

示例

包含了一些可缓存实体的示例,以了解如何

*过时缓存技术是一种避免任何面向用户的请求命中缓存未命中情况的方法。您可以在 博客文章 中了解更多关于它及其优缺点。

变更日志

请参阅 变更日志 了解最近发生了什么变化。

许可证

MIT 许可证 (MIT)。有关更多信息,请参阅 许可证文件