Реализация кеширующего класса в PHP: выбор оптимального подхода
Различные подходы к созданию класса кеширования в PHP
Кеширование позволяет сократить время выполнения скриптов за счет сохранения промежуточных результатов. Рассмотрим несколько способов реализации PHP классов для этой задачи.
Как создать простой файловый класс кеширования с истечением времени?
Вариант на основе файлов подходит для проектов без доступа к внешним серверам памяти. Пример реализации:
class FileCache {
private $cacheDir;
private $defaultTTL = 3600;
public function __construct($cacheDir, $defaultTTL = 3600) {
$this->cacheDir = rtrim($cacheDir, '/') . '/';
$this->defaultTTL = $defaultTTL;
if (!is_dir($this->cacheDir)) {
mkdir($this->cacheDir, 0755, true);
}
}
public function get($key) {
$file = $this->cacheDir . md5($key) . '.cache';
if (!file_exists($file)) return null;
$data = file_get_contents($file);
$expires = (int) substr($data, 0, strpos($data, "\n"));
if ($expires < time()) {
unlink($file);
return null;
}
return unserialize(substr($data, strpos($data, "\n") + 1));
}
public function set($key, $value, $ttl = null) {
$ttl = $ttl ?? $this->defaultTTL;
$file = $this->cacheDir . md5($key) . '.cache';
$expires = time() + $ttl;
$data = $expires . "\n" . serialize($value);
file_put_contents($file, $data, LOCK_EX);
}
}
Redis php mysql (использование redis с php и mysql)
Пояснения: метод set сохраняет значение с временем жизни, get проверяет срок годности и удаляет устаревший файл. Ключи хешируются через md5 для безопасного имени файла.
Типичные проблемы: одновременный доступ к файлам приводит к гонкам; на высоконагруженных проектах файловые операции медленны. Решение - использовать блокировку LOCK_EX в set и проверять наличие блокировки в get, либо перейти к in-memory кешу.
Как использовать APCu для создания in-memory кеша в классе?
APCu хранит данные в общей памяти интерпретатора. Класс будет тонкой оберткой:
class ApcuCache {
public function get($key, $default = null) {
$success = false;
$value = apcu_fetch($key, $success);
return $success ? $value : $default;
}
public function set($key, $value, $ttl = 3600) {
return apcu_store($key, $value, $ttl);
}
public function delete($key) {
return apcu_delete($key);
}
public function clear() {
return apcu_clear_cache();
}
}
Php class cache (класс для кеширования в php)
У APCu есть ограничение на размер данных (по умолчанию 1МБ на ключ) и необходимость расширения PHP.
Проблема: APCu не работает в CLI режиме по умолчанию; данные теряются при перезапуске PHP-FPM. Решение: использовать для долгоживущих данных файлы или Redis.
Как подключить Redis через PHP класс для распределенного кеша?
class RedisCache {
private $redis;
public function __construct($host = '127.0.0.1', $port = 6379) {
$this->redis = new \Redis();
$this->redis->connect($host, $port);
}
public function get($key, $default = null) {
$data = $this->redis->get($key);
return $data ? unserialize($data) : $default;
}
public function set($key, $value, $ttl = 3600) {
return $this->redis->setex($key, $ttl, serialize($value));
}
public function delete($key) {
return $this->redis->del($key) > 0;
}
public function clear() {
return $this->redis->flushDB();
}
}
Php file cache (кеширование файлов в php)
Redis поддерживает сложные структуры данных, но требует установки сервера.
Ошибка: забыли обработать исключения при подключении. Решение - обернуть вызовы в try-catch и реализовать fallback.
Как создать универсальный класс кеширования с возможностью смены драйвера?
Наиболее гибкое решение - реализация с интерфейсом и фабрикой. Пример интерфейса:
interface CacheInterface {
public function get($key, $default = null);
public function set($key, $value, $ttl = null);
public function delete($key);
public function clear();
}
Реализация для файлов (FileCache), APCu (ApcuCache) и Redis (RedisCache). Фабричный класс создает нужный экземпляр по конфигурации:
class CacheFactory {
public static function create($type, $config = []) {
switch ($type) {
case 'file':
return new FileCache($config['dir'] ?? sys_get_temp_dir());
case 'apcu':
return new ApcuCache();
case 'redis':
return new RedisCache($config['host'] ?? '127.0.0.1', $config['port'] ?? 6379);
default:
throw new \InvalidArgumentException("Unknown cache type");
}
}
}
Использование: $cache = CacheFactory::create('redis');.
Распространенные ошибки: неверный тип драйвера, отсутствие проверки наличия расширения. Решение - добавить проверки в фабрику и предоставлять fallback на файловый кеш.
Как реализовать инвалидацию кеша по тегам?
Теги позволяют удалять группы кешей. В файловой системе можно хранить индекс тегов:
class TaggedFileCache extends FileCache {
public function set($key, $value, $tags = [], $ttl = null) {
parent::set($key, $value, $ttl);
foreach ($tags as $tag) {
$tagFile = $this->cacheDir . md5('tag_' . $tag) . '.tag';
$keys = file_exists($tagFile) ? unserialize(file_get_contents($tagFile)) : [];
$keys[] = $key;
file_put_contents($tagFile, serialize($keys), LOCK_EX);
}
}
public function clearByTag($tag) {
$tagFile = $this->cacheDir . md5('tag_' . $tag) . '.tag';
if (!file_exists($tagFile)) return;
$keys = unserialize(file_get_contents($tagFile));
foreach ($keys as $key) {
$this->delete($key);
}
unlink($tagFile);
}
}
Для APCu или Redis теги проще реализовать через SET или SADD.
Проблема: в файловой версии при большом количестве ключей файл тега становится огромным. Решение - использовать базу данных (Redis Set).
Расширенные примеры кода класса кеширования
Пример 1: Полная реализация файлового кеша с блокировками и обработкой ошибок.
class SafeFileCache {
private $cacheDir;
private $defaultTTL;
public function __construct($dir, $defaultTTL = 3600) {
$this->cacheDir = rtrim($dir, '/') . '/';
$this->defaultTTL = $defaultTTL;
if (!is_dir($this->cacheDir)) {
if (!mkdir($this->cacheDir, 0755, true)) {
throw new \RuntimeException("Cannot create cache directory");
}
}
}
private function cacheFile($key) {
return $this->cacheDir . md5($key) . '.cache';
}
public function get($key, $default = null) {
$file = $this->cacheFile($key);
if (!file_exists($file)) return $default;
$fp = fopen($file, 'r');
if (!flock($fp, LOCK_SH)) {
fclose($fp);
return $default;
}
$data = stream_get_contents($fp);
fclose($fp);
$decoded = unserialize($data);
if ($decoded === false || $decoded['expires'] < time()) {
@unlink($file);
return $default;
}
return $decoded['value'];
}
public function set($key, $value, $ttl = null) {
$ttl = $ttl ?? $this->defaultTTL;
$file = $this->cacheFile($key);
$data = serialize([
'expires' => time() + $ttl,
'value' => $value
]);
file_put_contents($file, $data, LOCK_EX);
}
public function delete($key) {
$file = $this->cacheFile($key);
if (file_exists($file)) @unlink($file);
}
public function clear() {
array_map('unlink', glob($this->cacheDir . '*.cache'));
}
}
Результат использования:
$cache = new SafeFileCache('/tmp/myapp_cache');
$cache->set('user_1', ['name' => 'Alice', 'age' => 30], 60);
$user = $cache->get('user_1');
var_dump($user);
// array(2) { ["name"]=> string(5) "Alice" ["age"]=> int(30) }
Пример 2: Класс для Redis с обработкой исключений и переподключением.
class RedisCacheAdvanced {
private $redis;
private $host;
private $port;
public function __construct($host, $port = 6379) {
$this->host = $host;
$this->port = $port;
$this->connect();
}
private function connect() {
try {
$this->redis = new \Redis();
$this->redis->connect($this->host, $this->port, 2.5);
} catch (\Exception $e) {
error_log('Redis connection failed: ' . $e->getMessage());
$this->redis = null;
}
}
public function get($key, $default = null) {
if (!$this->redis) return $default;
try {
$data = $this->redis->get($key);
return $data ? unserialize($data) : $default;
} catch (\Exception $e) {
error_log('Redis get error: ' . $e->getMessage());
return $default;
}
}
public function set($key, $value, $ttl = 3600) {
if (!$this->redis) return false;
try {
return $this->redis->setex($key, $ttl, serialize($value));
} catch (\Exception $e) {
error_log('Redis set error: ' . $e->getMessage());
return false;
}
}
public function delete($key) {
if (!$this->redis) return false;
try {
return $this->redis->del($key) > 0;
} catch (\Exception $e) {
error_log('Redis delete error: ' . $e->getMessage());
return false;
}
}
public function clear() {
if (!$this->redis) return false;
try {
return $this->redis->flushDB();
} catch (\Exception $e) {
error_log('Redis flush error: ' . $e->getMessage());
return false;
}
}
}
Результат:
$cache = new RedisCacheAdvanced('localhost');
$cache->set('test_key', 'Hello World', 10);
echo $cache->get('test_key'); // Hello World
sleep(11);
echo $cache->get('test_key'); // (пустая строка или null)
Пример 3: Кеш с тегами на Redis с использованием SADD и SMEMBERS.
class RedisTaggedCache {
private $redis;
public function __construct($redis) {
$this->redis = $redis;
}
public function set($key, $value, $tags = [], $ttl = 3600) {
$this->redis->setex($key, $ttl, serialize($value));
foreach ($tags as $tag) {
$this->redis->sAdd('tag:' . $tag, $key);
}
}
public function get($key, $default = null) {
$data = $this->redis->get($key);
return $data ? unserialize($data) : $default;
}
public function clearByTag($tag) {
$keys = $this->redis->sMembers('tag:' . $tag);
foreach ($keys as $key) {
$this->redis->del($key);
}
$this->redis->del('tag:' . $tag);
}
}
Результат:
$redis = new Redis();
$redis->connect('127.0.0.1');
$cache = new RedisTaggedCache($redis);
$cache->set('article_1', 'Content 1', ['news', 'sports']);
$cache->set('article_2', 'Content 2', ['news']);
$cache->clearByTag('news');
echo $cache->get('article_1'); // пусто (удалено)
echo $cache->get('article_2'); // пусто (удалено)