Кеширование MySQL данных через Redis в PHP: практическое применение

Раздел: Веб-разработка -> Кеширование

Основные подходы к кешированию MySQL данных с Redis в PHP

Наиболее эффективное решение: комбинированное кеширование с помощью паттерна Cache-Aside и автоматической инвалидацией.

Данный подход подразумевает, что PHP скрипт сначала проверяет наличие данных в Redis по определённому ключу. Если данные есть и не истекло время жизни (TTL), они возвращаются из кеша. В противном случае выполняется дорогостоящий запрос к MySQL, результат сохраняется в Redis с TTL, и возвращается пользователю. При любом изменении данных (добавление, редактирование, удаление) соответствующий кеш принудительно удаляется или помечается как невалидный. Это позволяет поддерживать актуальность информации и одновременно снижать нагрузку на базу данных.


// Пример подключения к Redis через PhpRedis
try {
    $redis = new Redis();
    $redis->connect('127.0.0.1', 6379);
    $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);
} catch (Exception $e) {
    // Обработка ошибки подключения
    error_log('Redis connection failed: ' . $e->getMessage());
}

function getUsersFromCache() {
    global $redis;
    $key = 'users:all';
    $cached = $redis->get($key);
    if ($cached !== false) {
        return unserialize($cached);
    }
    // Данных в кеше нет – идём в MySQL
    $pdo = new PDO('mysql:host=127.0.0.1;dbname=test;charset=utf8', 'user', 'password');
    $stmt = $pdo->query('SELECT id, name, email FROM users WHERE active = 1');
    $users = $stmt->fetchAll(PDO::FETCH_ASSOC);
    $redis->setex($key, 300, serialize($users)); // TTL 5 минут
    return $users;
}

$users = getUsersFromCache();

Redis php mysql (использование redis с php и mysql)

Возможные проблемы и их решение:

  • Устаревшие данные: если время жизни кеша велико, данные могут быть неактуальны. Решение – установка разумного TTL (например, 30-300 секунд) в зависимости от частоты изменений.
  • Перегрузка Redis: слишком большое количество ключей может привести к нехватке памяти. Рекомендуется использовать volatile-lru или allkeys-lru политику вытеснения.
  • Проблемы с сериализацией: при больших объёмах данных сериализация может быть медленной. Альтернативы – сохранение в JSON или использование упакованных форматов (MsgPack).

Как кешировать результаты SQL запроса с автоматическим обновлением при изменении данных?

В этом варианте используется та же схема Cache-Aside, но инвалидация происходит принудительно в момент изменения сущности. Например, при запуске скрипта обновления профиля пользователя удаляется кеш, содержащий этого пользователя.


// При обновлении пользователя
function updateUser($id, $data) {
    global $pdo, $redis;
    // Обновление в MySQL
    $stmt = $pdo->prepare('UPDATE users SET name = :name WHERE id = :id');
    $stmt->execute([':name' => $data['name'], ':id' => $id]);
    // Удаляем кеш для конкретного пользователя и, возможно, список всех
    $redis->del("user:{$id}");
    $redis->del('users:all');
}

Php class cache (класс для кеширования в php)

Типичная ошибка: забывают удалять связанные кеши (например, список всех пользователей). Рекомендуется использовать паттерн «теги кеша» или вести реестр ключей, которые нужно очистить.

Как реализовать кеширование сессий на Redis вместо файлов?

PHP позволяет легко переключить хранилище сессий на Redis, что даёт возможность масштабировать приложение на нескольких серверах и ускорить доступ к сессиям.


// В php.ini или через ini_set()
ini_set('session.save_handler', 'redis');
ini_set('session.save_path', 'tcp://127.0.0.1:6379?prefix=SESSION_');
// Если используется пароль:
// ini_set('session.save_path', 'tcp://127.0.0.1:6379?auth=mypassword&prefix=SESSION_');

// В PHP коде всё остаётся неизменным
session_start();
$_SESSION['user_id'] = 123;

Php file cache (кеширование файлов в php)

Проблемы: при использовании сериализации PHP-сессий вместе с Redis нужно убедиться, что версии PHP на серверах одинаковы. Также возможна потеря сессии при переполнении памяти Redis – настройте политику вытеснения volatile-ttl.

Как кешировать HTML фрагменты страниц для уменьшения времени ответа?

Кеширование целых блоков HTML (например, меню, футер) снижает нагрузку на генерацию и ускоряет вывод. Используется буферизация вывода и сохранение результата в Redis.


function cachedView($key, callable $callback, $ttl = 600) {
    global $redis;
    $html = $redis->get($key);
    if ($html !== false) {
        return $html;
    }
    ob_start();
    $callback();
    $html = ob_get_clean();
    $redis->setex($key, $ttl, $html);
    return $html;
}

// Использование:
echo cachedView('footer:main', function() {
    include 'footer.php';
});

Ошибка: игнорирование персонализированных данных (например, имя пользователя в меню). Для таких случаев нужно кешировать шаблон без динамики или разбивать на несколько ключей.

Как использовать Redis для организации очередей и инвалидации кеша через Pub/Sub?

Механизм публикации/подписки позволяет синхронизировать кеш между несколькими серверами: при изменении данных в MySQL публикуется сообщение, а все подписчики сбрасывают соответствующие кеши.


// Скрипт публикации при обновлении
$redis->publish('cache:invalidate', json_encode(['key' => 'users:all']));

// Скрипт-воркер, который постоянно слушает канал
$redis->subscribe(['cache:invalidate'], function($redis, $channel, $message) {
    $data = json_decode($message, true);
    if (isset($data['key'])) {
        $redis->del($data['key']);
    }
});

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

Расширенные примеры использования Redis с PHP и MySQL

Класс для гибкого кеширования с поддержкой тегов

Реализация на основе PhpRedis, позволяющая инвалидировать группы записей по одному тегу.

Пример

class RedisCache
{
    private $redis;
    private $prefix = 'cache:';

    public function __construct(Redis $redis) {
        $this->redis = $redis;
    }

    public function set($key, $data, $tags = [], $ttl = 3600) {
        $key = $this->prefix . $key;
        $this->redis->setex($key, $ttl, serialize($data));
        foreach ($tags as $tag) {
            $tagKey = $this->prefix . 'tag:' . $tag;
            $this->redis->sAdd($tagKey, $key);
            $this->redis->expire($tagKey, $ttl + 86400); // храним теги дольше
        }
    }

    public function get($key) {
        $key = $this->prefix . $key;
        $data = $this->redis->get($key);
        return $data === false ? false : unserialize($data);
    }

    public function invalidateByTag($tag) {
        $tagKey = $this->prefix . 'tag:' . $tag;
        $keys = $this->redis->sMembers($tagKey);
        if ($keys) {
            $this->redis->del($keys);
            $this->redis->del($tagKey);
        }
    }
}

// Применение
$cache = new RedisCache($redis);
$cache->set('user:1', ['id'=>1, 'name'=>'Alice'], ['user', 'user:1']);
$data = $cache->get('user:1');
// Инвалидация всех пользователей
$cache->invalidateByTag('user');
Результат: после invalidateByTag('user') ключи 'user:1' удалены, следующий вызов get вернёт false.

Использование Lua-скриптов для атомарного обновления

Скрипт, загружаемый в Redis, гарантирует, что данные из MySQL попадут в кеш только один раз (защита от гонок).

Пример

<?
// Код PHP загрузки и выполнения
$script = <<script('load', $script);
$result = $redis->evalSha($sha, ['user:1', 3600, serialize($userData)], 1);
Результат: если ключ уже существует – возвращается старое значение, иначе – новое, записанное в Redis.

Использование очередей на основе списков для фоновой записи

Вместо прямого обновления MySQL можно помещать задачу в Redis spool, а обработчик периодически сохраняет изменения в базу.

Пример

// Добавление задачи (например, при просмотре страницы)
$redis->rPush('log:views', json_encode(['page'=>$_SERVER['REQUEST_URI'], 'time'=>time()]));

// Демон-обработчик (запускается как отдельный процесс)
while (true) {
    $data = $redis->blPop('log:views', 0); // блокирующее чтение
    $entry = json_decode($data[1], true);
    // Вставка в MySQL
    $pdo->prepare('INSERT INTO views (page, viewed_at) VALUES (?, ?)')->execute([$entry['page'], date('Y-m-d H:i:s', $entry['time'])]);
}
Результат: нагрузка на MySQL сглаживается, записи происходят пачками по мере поступления из очереди.

Сравнение производительности с кешем и без

Пример замера времени выполнения запроса с использованием микровремени.

Пример

// Без кеша
$start = microtime(true);
$pdo->query('SELECT * FROM big_table')->fetchAll();
echo 'Без кеша: ' . (microtime(true) - $start) . ' сек';

// С кешем
$start = microtime(true);
if (($data = $redis->get('big_table')) === false) {
    $data = $pdo->query('SELECT * FROM big_table')->fetchAll();
    $redis->setex('big_table', 60, serialize($data));
}
$result = unserialize($data);
echo 'С кешем: ' . (microtime(true) - $start) . ' сек';
Пример вывода:
Без кеша: 0.245 сек
С кешем: 0.003 сек

Настройка Redis Cluster для отказоустойчивости

Пример создания клиента с поддержкой кластеризации на базе phpredis.

Пример

$redis = new RedisCluster(null, [
    '192.168.1.10:7000',
    '192.168.1.11:7000',
    '192.168.1.12:7000'
], 1.5, 1.5, true); // timeout, readTimeout, persistent
$redis->set('key', 'value', 3600);
echo $redis->get('key');
Результат: данные автоматически распределяются и реплицируются между узлами.

Использование Redis с PHP и MySQL - comments

En
Redis php mysql (php)