Кеширование объектов: от APCu до файлов

Раздел: PHP -> Кеширование в PHP

Кеширование объектов в PHP: обзор

Объектный кеш позволяет сохранять экземпляры классов в памяти или на диске для повторного использования, избегая затратных операций создания или загрузки. В PHP доступно несколько решений – от встроенных расширений до внешних хранилищ. Рассмотрим основные варианты, их преимущества и ограничения.

Какое решение является самым быстрым для кеширования объектов в памяти?

APCu – расширение для кеширования в общей памяти процесса. Данные хранятся в памяти и доступны всем процессам PHP. Для использования необходимо установить расширение apcu (например, pecl install apcu). APCu автоматически сериализует объекты, поэтому класс должен быть загружен до десериализации. Пример:

// Сохранение объекта
$user = new User(1, 'Alice');
apcu_store('user_1', $user, 3600); // TTL 1 час

// Получение
$cachedUser = apcu_fetch('user_1');
if ($cachedUser === false) {
    // объект не найден или истек
}
// Проверка существования
if (apcu_exists('user_1')) { ... }

Object cache php (объектный кеш php)

Проблема: при изменении класса (добавление/удаление свойств) старые объекты из кеша могут вызвать ошибку при десериализации. Решение: использовать префикс ключа с версией класса или очищать кеш при обновлении кода.

Ошибка: PHP Fatal error: Class 'User' not found. Возникает, если класс не определен до вызова apcu_fetch. Решение: подключить автозагрузку (composer autoload) перед получением данных.

APCu подходит для одностороннего кеширования внутри одного сервера. Не работает в кластерных конфигурациях.

Как сохранить объект только в рамках одного запроса?

Статическое свойство класса – примитивный кеш, не требующий внешних хранилищ. Данные существуют только во время выполнения скрипта. Пример:

class UserProvider {
    private static array $cache = [];

    public static function getUser(int $id): User {
        if (isset(self::$cache[$id])) {
            return self::$cache[$id];
        }
        $user = User::find($id); // дорогая операция
        self::$cache[$id] = $user;
        return $user;
    }
}

Php clear cache (очистка кеша php)

Проблема: нет автоматической очистки по времени. Решение: контролировать вручную или использовать счётчик запросов.

Используется для избежания повторных вызовов методов внутри одного HTTP-запроса (например, при рендеринге нескольких блоков).

Как организовать распределённый объектный кеш?

Redis – внешнее хранилище, доступное по сети. Требует установки сервера Redis и расширения redis или predis. Объекты сериализуются в строку. Пример с phpredis:

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$key = 'user:' . $userId;
$data = $redis->get($key);
if ($data === false) {
    $user = User::find($userId);
    $redis->setex($key, 3600, serialize($user));
} else {
    $user = unserialize($data);
}

Проблема: сериализация/десериализация может быть медленной. Решение: использовать igbinary для быстрой сериализации (если установлен). Ошибка: превышение лимита памяти Redis. Решение: настроить политику вытеснения (eviction).

Redis подходит для кластеров, нескольких серверов, где требуется общий кеш.

Как кешировать объекты без дополнительных расширений?

Файловый кеш – сериализация объекта в файл. Пример:

$cacheFile = sys_get_temp_dir() . '/user_' . $userId . '.cache';
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < 3600) {
    $user = unserialize(file_get_contents($cacheFile));
} else {
    $user = User::find($userId);
    file_put_contents($cacheFile, serialize($user));
}

Проблема: низкая производительность при большом количестве файлов. Решение: использовать хэш-директории или базу данных (например, SQLite). Ошибка: конкурентный доступ – два запроса одновременно пишут в один файл. Решение: блокировка файла (flock).

Файловый кеш удобен на хостингах без доступа к расширениям, но неэффективен для высоких нагрузок.

Расширенные примеры объектного кеширования

Пример 1: APCu с обработкой ошибок и версионированием

Пример
// Определение класса User (для примера)
class User {
    public int $id;
    public string $name;
    public function __construct(int $id, string $name) {
        $this->id = $id;
        $this->name = $name;
    }
}

// Версия класса для инвалидации кеша
const CLASS_VERSION = 'v2';

function getUserFromCache(int $userId): ?User {
    $key = 'user_' . CLASS_VERSION . '_' . $userId;
    $cached = apcu_fetch($key, $success);
    if ($success) {
        return $cached;
    }
    // Имитация долгой загрузки
    $user = new User($userId, 'User_' . $userId);
    apcu_store($key, $user, 300); // 5 минут
    return $user;
}

$user = getUserFromCache(42);
echo $user->name;
Вывод: User_42
(при повторном запуске apcu_fetch вернет объект без повторного создания)

Пример 2: Redis с сериализацией igbinary

Пример
// Предполагается установленное расширение igbinary
$redis = new Redis();
$redis->connect('localhost');
$redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_IGBINARY);

$key = 'user:42';
$cached = $redis->get($key);
if ($cached === false) {
    $user = new User(42, 'Bob');
    $redis->setex($key, 3600, $user);
    echo "Сохранено в Redis\n";
} else {
    echo "Получено из Redis: " . $cached->name . "\n";
}
Первый запуск: Сохранено в Redis
Второй запуск: Получено из Redis: Bob

Пример 3: Статический кеш для дорогого метода

Пример
class ExpensiveCalculator {
    private static array $results = [];

    public static function compute(int $input): int {
        if (isset(self::$results[$input])) {
            echo "Возврат из кеша\n";
            return self::$results[$input];
        }
        // Имитация тяжелых вычислений
        sleep(2);
        $result = $input * $input;
        self::$results[$input] = $result;
        echo "Вычислено\n";
        return $result;
    }
}

echo ExpensiveCalculator::compute(5) . "\n";
echo ExpensiveCalculator::compute(5) . "\n";
Вычислено
25
Возврат из кеша
25

Пример 4: Файловый кеш с блокировкой и проверкой TTL

Пример
function fileCacheCallback(int $userId, callable $loader): User {
    $cacheDir = sys_get_temp_dir() . '/objcache';
    if (!is_dir($cacheDir)) {
        mkdir($cacheDir, 0777, true);
    }
    $file = $cacheDir . '/user_' . $userId . '.cache';
    $ttl = 3600;

    // Чтение с блокировкой
    $fp = fopen($file, 'r');
    if ($fp && flock($fp, LOCK_SH)) {
        if (time() - filemtime($file) < $ttl) {
            $data = stream_get_contents($fp);
            flock($fp, LOCK_UN);
            fclose($fp);
            $user = unserialize($data);
            if ($user !== false) {
                return $user;
            }
        }
        flock($fp, LOCK_UN);
        fclose($fp);
    }

    // Запись с эксклюзивной блокировкой
    $user = $loader($userId);
    $fp = fopen($file, 'w');
    if ($fp && flock($fp, LOCK_EX)) {
        fwrite($fp, serialize($user));
        flock($fp, LOCK_UN);
        fclose($fp);
    }
    return $user;
}

$user = fileCacheCallback(42, function($id) {
    return new User($id, 'Charlie');
});
echo $user->name;
Вывод: Charlie (при повторном вызове в течение часа - из файла)

Объектный кеш PHP - comments

En
Object cache php (php)