Реализация кеширующего класса в 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'); // пусто (удалено)

Класс для кеширования в PHP - comments

En
Php class cache (php)