使用缓存来提升 PHP 项目

2025-06-08

使用缓存来提升 PHP 项目

项目的增长及其负载对开发人员来说是一个真正的挑战。网站响应开始出现延迟,扩展问题也越来越频繁地被提出。有许多有效的解决方案可以提高项目的稳定性和负载能力,其中最基本的方法之一就是缓存。

缓存是指将数据临时保存在易于访问的位置,以便比从源头检索更快。使用缓存的最常见示例是从数据库获取数据。例如,当首次从数据库接收到某个产品时,它会在缓存中存储一​​段时间,因此后续对该产品的每个请求都不会干扰数据库,因为数据将从另一个存储中接收。

有哪些方法?

缓存方法有很多种。您可以在php-cache页面上查看兼容 PHP 的工具列表。不过,最常用的是以下几种:

  • 亚太铜
  • 大批
  • Memcached
  • Redis

让我们看看它们的区别和特点。

亚太铜

最常用且易于配置的缓存工具之一。它将数据保存到 RAM 中(并且知道如何缓存中间代码,但那是完全不同的故事……)。要开始使用 APCu,您需要确保已安装它。为此,请在命令行中运行以下命令:

php -i | grep 'apc.enabled'
# We're expecting to see:
# apc.enabled => On => On
Enter fullscreen mode Exit fullscreen mode

另一种检查方法是:创建一个index.php文件,并将phpinfo()函数调用放入其中。确保已为正在使用的目录配置了 Web 服务器,并通过服务器地址在浏览器中打开脚本。我们关注的是 APCu 部分,如果其中有“APCu Support: Enabled”项,则表示一切正常,我们可以继续。

图像

如果您没有安装 APCu,您可以按照以下方式安装:

  1. 启动终端窗口(Linux/macOS)或命令提示符(Windows。在搜索中输入“cmd”)
  2. 运行以下命令:pecl install apcu apcu_bc
  3. 通过任何文本编辑器打开php.ini配置文件,并确保存在以下行:

    # Windows
    extension=php_apcu.dll
    extension=php_apcu_bc.dll
    
    apc.enabled=1
    apc.enable_cli=1
    
    #Linux / MacOS
    extension="apcu.so"
    extension="apc.so"
    
    apc.enabled=1
    apc.enable_cli=1
    
  4. 如果没有指定的行,则添加并保存配置文件

  5. 重复检查已安装的 APCu

要使用这种缓存方法,我们需要一些基本函数。以下是它们的应用示例。

$cacheKey = 'product_1';
$ttl = 600; // 10 minutes.

// Checking APCu availability
$isEnabled = apcu_enabled();

// Checks if there is data in the cache by key
$isExisted = apcu_exists($cacheKey);

// Saves data to the cache. Returns true if successful
// The $ttl argument determines how long the cache will be stored (seconds)
$isStored = apcu_store($cacheKey, ['name' => 'Demo product'], $ttl);

// Retrieves data from the cache by key. If not, returns false
$data = apcu_fetch($cacheKey);

// Deletes data from the cache by key
$isDeleted = apcu_delete($cacheKey);

var_dump([
    'is_enabled'   => $isEnabled,
    'is_existed'   => $isExisted,
    'is_stored'    => $isStored,
    'is_deleted'   => $isDeleted,
    'fetched_data' => $data,
]);
Enter fullscreen mode Exit fullscreen mode

任何缓存都基于键值存储的原理,这意味着存储的数据都存储在一个特殊的键中,以便访问。在这种情况下,键存储在$cacheKey变量中。

重要提示!此方法仅适用于网站模式,即从命令行运行时,您将无法从缓存中获取数据,并且脚本执行完成后,您保存到缓存中的所有内容都将被清除。但是,这不会导致任何错误。

数组缓存

一种更简单但并非总是适用的缓存方法。如果 APCu 保存数据并使其可供所有进程后续执行,则数组缓存仅针对正在处理的请求存储数据。

这是什么意思呢?假设你有一个包含用户评论的页面。一个用户可以留下多条评论,当我们收集这些数据的数组时,我们不希望从数据库中多次获取同一个用户的数据。我们可以做的是将接收到的数据设置为一个数组,这样,如果有这样的数组,我们就不需要再次发出请求。这个原则非常简单,也很容易实现。让我们编写一个类来执行这样的保存操作。

class CustomArrayCache
{
    /**
     * Array is static and private
     * – private – so that it can be accessed only from the
     * methods of the class
     * – static – so that the property is available in all instances
     */
    private static array $memory = [];

    // Method for storing data in memory
    public function store(string $key, $value): bool
    {
        self::$memory[$key] = $value;

        return true;
    }

    // Method for getting data from memory
    public function fetch(string $key)
    {
        return self::$memory[$key] ?? null;
    }

    // Method for deleting data from memory
    public function delete(string $key): bool
    {
        unset(self::$memory[$key]);

        return true;
    }

    // Method for checking the availability of key data
    public function exists(string $key): bool
    {
        return array_key_exists($key, self::$memory);
    }
}
Enter fullscreen mode Exit fullscreen mode

这种方法非常简单,但由于其局限性,很少使用。不过,了解一下还是很有用的。

Memcached 和 Redis

最先进的缓存方法。它们需要单独运行一个服务器(守护进程)。我们从 PHP 连接到该服务器的地址和端口。这些解决方案的配置比设置 APCu 更复杂,但数据存储方式非常相似(RAM)。它们最重要的优势在于:

  • 与 PHP 隔离。单独的服务负责缓存;
  • 集群能力。如果你的项目负载非常高,缓存服务的集群将有助于应对这一问题;

在本文中,我们不会详细介绍 Memcached 和 Redis 的配置。在此阶段,我们需要记住,如果负载非常高,我们应该考虑这些解决方案,因为它们具有良好的扩展潜力。

PSR-16 标准

PSR 有两个专用于缓存的标准:PSR-6(普通缓存接口)和PSR-16(简单缓存接口)——我们将重点关注 PSR-16。

该标准提供了一个特殊的接口(CacheInterface),执行缓存功能的类可以满足该接口。根据该接口,这些类应该实现以下方法:

  • get($key, $default)- 从缓存中获取数据。第二个参数传递的是数据缺失时返回的值。
  • set($key, $value, $ttl = null)- 将数据保存到缓存。如前所述,第三个参数传递的是存储时间(以秒为单位)。如果为空 (null),则将使用缓存配置中的默认值。
  • delete($key)- 删除关键数据;
  • clear()- 清除整个存储;
  • getMultiple($keys, $default)- 允许您一次获取多个键的数据;
  • setMultiple($values, $ttl = null)- 允许您一次记录多个值。$value我们传递一个关联数组,其中键表示$key缓存,值表示要保存的数据;
  • deleteMultiple($keys)- 删除多个键的数据;
  • has($key)- 检查关键数据的可用性。

如你所见,接口非常简单,甚至我们在 APCu 示例​​中考虑的功能也足以让你按照 PSR-16 编写缓存服务。但为什么有必要这样做呢?

遵守PSR标准的主要优势:

  • 它们受到最流行的图书馆的支持;
  • 许多 PHP 程序员遵守 PSR 并且会很容易地习惯你的代码;
  • 通过该接口,我们可以轻松地将所使用的服务更改为任何其他支持 PSR-16 的服务。

让我们仔细看看最后一点及其好处。

PSR-16 库的使用

在现有缓存​​工具上创建“包装器”以匹配接口的库称为适配器。例如,考虑我们已经讨论过的方法的适配器:

它们都满足 PSR-16,因此以相同的方式应用,但每个都有自己的“底层”逻辑。

作为使用示例,让我们使用Composer将APCu数组适配器加载到我们的项目中。

composer require cache/array-adapter
composer require cache/apcu-adapter
# Or
composer req cache/apcu-adapter cache/array-adapter
Enter fullscreen mode Exit fullscreen mode

假设我们有一个专门用于从数据库中获取产品的类,我们称之为 ProductRepository,它有一个find($id)方法,根据 ID 返回产品,如果不存在则返回 null。

class ProductRepository
{
    /**
     * In order not to complicate the example, we will stipulate that
     * an array is returned as a product, and if it is not, null
     */
    public function find(int $id): ?array
    {
        // ...
        // Getting data from the database
        return $someProduct;
    }
}
Enter fullscreen mode Exit fullscreen mode

如果我们想要启用缓存,不应该在存储库内部进行,因为它的职责是从数据库返回数据。那么,我们应该在哪里添加缓存呢?有几种流行的解决方案,最简单的方法是添加一个额外的提供程序类。它所做的就是尝试从缓存中获取数据,如果获取不到,它就会联系存储库。为此,我们将在此类的构造函数中定义两个依赖项——我们的存储库和CacheInterface。为什么要使用接口?因为我们将能够使用任何提到的适配器或其他符合 PSR-16 的类。

use Psr\SimpleCache\CacheInterface;

class ProductDataProvider
{
    private ProductRepository $productRepository;
    private CacheInterface $cache;

    public function __construct(ProductRepository $productRepository, CacheInterface $cache)
    {
        $this->productRepository = $productRepository;
        $this->cache             = $cache;
    }

    public function get(int $productId): ?array
    {
        $cacheKey = sprintf('product_%d', $productId);

        // Trying to get the product from the cache
        $product = $this->cache->get($cacheKey);
        if ($product !== null) {
            // If there is a product, we return it
            // Temporarily output echo to understand that the data is from the cache
            echo 'Data from cache' . PHP_EOL; // PHP_EOL is a line break
            return $product;
        }
        // If there is no product, we get it from the repository
        $product = $this->productRepository->find($productId);

        if ($product !== null) {
            // Now we will save the received product to the cache for future requests
            // Also temporarily output echo
            echo 'Data from DB' . PHP_EOL;
            $this->cache->set($cacheKey, $product);
        }

        return $product;
    }
}
Enter fullscreen mode Exit fullscreen mode

我们的类已经准备好了。现在让我们看看它与 APCu 适配器结合使用的情况。

use Cache\Adapter\Apcu\ApcuCachePool;

// Connect the Composer autoloader
require_once 'vendor/autoload.php';

// Our repository
$productRepository = new ProductRepository();
// APCu-cache adapter. Does not require any additional settings
$cache = new ApcuCachePool();

// Creating a provider, passing dependencies
$productDataProvider = new ProductDataProvider(
    $productRepository,
    $cache
);

// If there is such a product in the database, it will come back to us
$product = $productDataProvider->get(1);
var_dump($product);
Enter fullscreen mode Exit fullscreen mode

如果我们想用数组适配器或任何其他方法替换 APCu 缓存,我们只需将其传递给提供程序,因为它们都实现了CacheInterface

use Cache\Adapter\PHPArray\ArrayCachePool;
// ...
$productRepository = new ProductRepository();
//$cache = new ApcuCachePool();
$cache = new ArrayCachePool();
$productDataProvider = new ProductDataProvider(
    $productRepository,
    $cache
);
// ...
Enter fullscreen mode Exit fullscreen mode

竞争条件和数据更新

只要我们保持缓存更新,缓存就能正常工作。这意味着,如果用户想要更新产品,就必须同时在数据库和缓存中更新。然而,这里有一个重要的细节。

想象一下,我们的项目被大量用户使用。其中两个用户同时更新同一个实体。在这种情况下,可能会出现以下情况:

  • 用户 1 从缓存中收到一个实体
  • 用户 1 更新了数据库中的实体
  • 用户 2 从缓存中获取实体
  • 用户1更新了缓存中的数据
  • 用户 2 更新了数据库中的实体,但用旧数据覆盖了它,因为该实体在接收时不相关
  • 等等...

当多个进程同时访问同一资源时,这种情况称为竞争条件,并且可能会发生版本冲突。为了避免此类问题,应遵循一条简单的规则:

当您在代码中接收任何实体并对其进行更新时,请始终使用数据库中的数据。

在任何情况下,当我们需要获取产品信息但又不打算更新时,我们都会使用缓存。如果要更新,则使用数据库中的数据。

ProductRepository您可以在正确的位置引用,而不是ProductDataProvider,或者向数据提供者的方法添加参数。例如($fromCache):

class ProductDataProvider
{
    // ...
    public function get(int $productId, bool $fromCache = true): ?array
    {
        $cacheKey = sprintf('product_%d', $productId);

        $product = $fromCache 
            ? $this->cache->get($cacheKey) 
            : null;
        if ($product !== null) {
            return $product;
        }
        $product = $this->productRepository->find($productId);

        if ($product !== null) {
            $this->cache->set($cacheKey, $product);
        }

        return $product;
    }
}
Enter fullscreen mode Exit fullscreen mode

结论

在开发项目时,缓存需要开发人员付出额外的努力,而且使用缓存并不总是合适的。是否使用缓存的决定应该基于预期(或实际)负载以及你对用户响应速度的期望。

但是,无论您是否会在当前项目中使用这些方法,都应该进行研究并付诸实践,因为这项技能在您大型团队中工作时会很有用。

总结一下这篇文章,我们来看看关键思想:

  • 遵守 PSR-16(或 PSR-6)将允许您轻松连接第三方库进行缓存,并使其他开发人员能够理解您的代码;
  • 对于小型项目来说,APCu 是一个很好的缓存解决方案,因为它易于配置并且使用 RAM,访问率非常高;
  • 对于所有与 PHP 兼容的缓存工具,可以在网站php-cache.com上查看一些适配器;
  • 缓存是单独的职责。尽量在单独的类中实现缓存相关功能;
  • 如果要更新实体,则应从数据库中获取。如果仅需要查看实体,则可以从缓存中请求;
  • 在大型项目中,使用Memcached或Redis来获得扩展能力。
鏂囩珷鏉ユ簮锛�https://dev.to/he110/boosting-up-php-project-with-cache-16hi
PREV
React:使用 React.memo、useMemo 和 useCallback 优化组件 GenAI LIVE!| 2025 年 6 月 4 日
NEXT
作为一名 Web 开发者,我的 Chrome 扩展程序