Кеширование объектов: от APCu до файлов
Кеширование объектов в 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 (при повторном вызове в течение часа - из файла)