Изучаем расширение php-redis: кэширование и очереди в PHP

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

Расширение Redis для PHP: установка и варианты использования

Основное эффективное решение

Наиболее распространённый способ установки расширения php-redis - через PECL. После установки создаётся экземпляр класса Redis и выполняются базовые операции.

sudo pecl install redis

Php redis extension (расширение redis для php)

Далее необходимо добавить строку extension=redis.so в файл php.ini и перезапустить веб-сервер или PHP-FPM.

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->set('key', 'value');
echo $redis->get('key'); // value

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

  • Ошибка «Redis server gone away» - сервер Redis не запущен или недоступен. Решение: проверить статус Redis командой redis-cli ping.
  • Ошибка «Class 'Redis' not found» - расширение не загружено. Решение: убедиться, что в php.ini присутствует extension=redis и файл .so расположен в директории расширений.
  • Проблемы с версией PHP - PECL может установить расширение для другой версии. Рекомендуется использовать pecl install redis с указанием версии PHP или системный менеджер пакетов.

Как установить php-redis через системный менеджер пакетов?

На Debian/Ubuntu доступен пакет php-redis. Этот способ проще, но версия может отставать от актуальной.

sudo apt-get install php-redis

На CentOS/RHEL используется пакет php-pecl-redis.

sudo yum install php-pecl-redis

Проблема: несовместимость с версией PHP из другого репозитория. Решение: добавить репозиторий, соответствующий установленной версии PHP (например, remi для CentOS).

Как использовать постоянное подключение к Redis (pconnect)?

Постоянное подключение позволяет избежать накладных расходов на установку соединения при каждом запросе. Используется метод pconnect.

$redis = new Redis();
$redis->pconnect('127.0.0.1', 6379, 2.5); // таймаут 2.5 сек
$redis->set('session', 'data');
echo $redis->get('session');

Важно: постоянное соединение живёт до завершения процесса PHP. При использовании PHP-FPM соединение сохраняется между запросами к одному воркеру.

Проблема: увеличение числа открытых соединений на сервере Redis. Решение: настроить maxclients в redis.conf и использовать пул соединений.

Как установить время жизни ключа (TTL) для автоматического удаления?

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

$redis->set('cache:page', 'html', 60); // установка с TTL 60 секунд
// Альтернатива: отдельно
echo $redis->expire('cache:page', 120); // (bool) true

Проверить оставшееся время можно через ttl.

echo $redis->ttl('cache:page'); // число секунд или -1 если без TTL

Ошибка: после изменения ключа TTL сбрасывается. Решение: обновлять TTL повторно при каждом изменении.

Как использовать Redis для организации очереди сообщений?

Redis списки идеально подходят для очередей. Методы lPush (добавление в начало) и rPop (извлечение с конца) реализуют простую очередь FIFO.

// Отправка задачи
$redis->lPush('queue:tasks', json_encode(['type' => 'email', 'to' => 'user@example.com']));

// Получение (блокирующее) 
$task = $redis->brPop('queue:tasks', 0); // ждёт бесконечно
$data = json_decode($task[1], true);

Блокирующая версия brPop ожидает появления элемента в очереди, что удобно для воркеров.

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

Как реализовать атомарный инкремент и его применение для счётчиков?

Метод incr атомарно увеличивает числовое значение ключа. Если ключ не существует, он создаётся с начальным значением 0.

$redis->incr('counter:visits');
$redis->incrBy('counter:visits', 10); // увеличение на заданное число
echo $redis->get('counter:visits');

Счётчики применяются для лимитов, статистики, рейтингов.

Проблема: переполнение 64-битного целого. Решение: использовать плавающие числа или отдельный ключ для сброса.

Как кэшировать сложные структуры данных через хэши?

Redis хэши (hashes) позволяют хранить и получать поля объекта. Это удобно для кэширования результатов SQL-запросов или ассоциативных массивов.

// Сохранение данных пользователя
$redis->hSet('user:123', 'name', 'Иван');
$redis->hSet('user:123', 'email', 'ivan@example.com');

// Получение всех полей
$user = $redis->hGetAll('user:123');
print_r($user); // ['name'=>'Иван', 'email'=>'ivan@example.com']

Хэши экономят память по сравнению с хранением каждого поля отдельно.

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

Как использовать pipeline для уменьшения задержек?

Pipeline позволяет отправить несколько команд Redis за один раз, не дожидаясь ответа на каждую. Это значительно ускоряет массовые операции.

$redis->pipeline();
for ($i = 0; $i < 1000; $i++) {
    $redis->set('key:'.$i, $i);
}
$replies = $redis->exec(); // возвращает массив ответов

Метод pipeline включает режим конвейера. Все команды буферизуются и отправляются при вызове exec.

Проблема: pipeline не гарантирует атомарности, в отличие от транзакций. Решение: использовать multi/exec для атомарных групп команд.

Как выполнять атомарные транзакции с MULTI/EXEC?

Транзакции Redis гарантируют, что все команды выполнятся последовательно без вмешательства других клиентов.

$redis->multi();
$redis->incr('counter');
$redis->expire('counter', 3600);
$results = $redis->exec(); // [1, true]

Команды в транзакции буферизуются и выполняются только после exec.

Проблема: транзакции не поддерживают откат (rollback) при синтаксической ошибке. Решение: проверять корректность команд до отправки.

Как работать с множествами (SET) для уникальных элементов?

Множества хранят уникальные строки и поддерживают операции пересечения, объединения, разности.

$redis->sAdd('tags:php', 'redis');
$redis->sAdd('tags:php', 'memcached');
$redis->sAdd('tags:php', 'redis'); // дубликат игнорируется
$members = $redis->sMembers('tags:php'); // ['redis', 'memcached']
$count = $redis->sCard('tags:php'); // 2

Применяется для тегов, уникальных посетителей, фильтрации.

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

Расширенные примеры работы с php-redis

Реализация распределённой блокировки с помощью SET NX EX и Lua-скрипта

Для предотвращения одновременного доступа к ресурсу используется блокировка на основе атомарных команд. Ключ создаётся только если его нет (NX) и с таймаутом (EX). Освобождение блокировки выполняется через Lua-скрипт, гарантирующий, что удалит ключ только владелец.

Пример
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$lockKey = 'lock:resource';
$lockValue = uniqid('', true);
$ttl = 10; // секунд

// Попытка получить блокировку
if ($redis->set($lockKey, $lockValue, ['NX', 'EX' => $ttl])) {
    // Критическая секция
    echo "Выполняется критическая операция...\n";
    sleep(2);
    
    // Освобождение блокировки (только если значение совпадает)
    $script = '
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end
    ';
    $result = $redis->eval($script, [$lockKey, $lockValue], 1);
    echo $result ? "Блокировка снята\n" : "Не удалось снять блокировку\n";
} else {
    echo "Блокировка не получена, ресурс занят\n";
}
Выполняется критическая операция...
Блокировка снята

Проблема: если блокировка удерживается дольше TTL, другой процесс может получить блокировку на тот же ресурс. Решение: использовать более короткий TTL и продлевать блокировку внутри критической секции (например, через фоновый воркер или проверку времени).

Использование Redis для хранения PHP-сессий через session_set_save_handler

Пользовательский обработчик сессий позволяет хранить сессии в Redis, что даёт быстрый доступ и масштабирование. Реализуется через класс, реализующий интерфейс SessionHandlerInterface.

Пример
class RedisSessionHandler implements SessionHandlerInterface {
    private $redis;
    private $prefix = 'session:'; // префикс для ключей
    private $ttl = 3600; // время жизни сессии

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

    public function open($savePath, $sessionName): bool {
        // Подключение уже установлено
        return true;
    }

    public function close(): bool {
        return true;
    }

    public function read($sessionId): string {
        return $this->redis->get($this->prefix . $sessionId) ?: '';
    }

    public function write($sessionId, $data): bool {
        return $this->redis->setex($this->prefix . $sessionId, $this->ttl, $data);
    }

    public function destroy($sessionId): bool {
        return $this->redis->del($this->prefix . $sessionId) > 0;
    }

    public function gc($maxlifetime): int {
        // Redis сам удаляет ключи по TTL
        return 0;
    }
}

// Регистрация обработчика
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
session_set_save_handler(new RedisSessionHandler($redis), true);
session_start();
$_SESSION['user'] = 'admin';
echo session_id();

Проблема: блокировка сессии при параллельных запросах. Решение: использовать специальные обработчики с блокировками (например, session_set_save_handler с аргументом read_and_close) или обернуть запись в блокировку Redis.

Rate Limiting (ограничение частоты запросов) с помощью INCR и TTL

Ограничение количества действий за интервал - типичная задача для Redis. Используется счётчик с TTL, который автоматически сбрасывается по истечении интервала.

Пример
function isRateLimited($userId, $maxRequests = 10, $perSeconds = 60) {
    $redis = new Redis();
    $redis->connect('127.0.0.1', 6379);
    
    $key = 'ratelimit:'.$userId;
    $current = $redis->incr($key);
    
    // При первом запросе устанавливаем TTL
    if ($current === 1) {
        $redis->expire($key, $perSeconds);
    }
    
    return $current > $maxRequests;
}

// Проверка
if (isRateLimited('user:1')) {
    http_response_code(429);
    echo 'Too Many Requests';
} else {
    echo 'OK';
}
OK

Проблема: гонка условий между incr и expire - возможна ситуация, когда TTL не установлен. Решение: использовать одну атомарную команду SET с NX и EX, увеличивая значение через INCR после. Более точный вариант - скрипт Lua, проверяющий существование ключа.

Кэширование результатов SQL запроса с инвалидацией по тегу

При кэшировании данных из базы полезно иметь возможность сбросить весь кэш для определённого тега (например, при обновлении сущности). Используется хэш с полями и отдельный ключ для тегов.

Пример
function getCachedData($key, $tags, $ttl, $callback) {
    $redis = new Redis();
    $redis->connect('127.0.0.1', 6379);
    
    // Проверяем, не протух ли кэш по тегам?
    // Храним версию тега и сравниваем с сохранённой версией в кэше
    $tagVersionKey = 'tag:versions';
    $cachedVersion = $redis->hGet($tagVersionKey, $key);
    
    $currentTagVersions = [];
    foreach ($tags as $tag) {
        $version = $redis->get('tag:'.$tag) ?: 0;
        $currentTagVersions[$tag] = $version;
    }
    $combinedVersion = md5(serialize($currentTagVersions));
    
    if ($cachedVersion === $combinedVersion) {
        $data = $redis->get($key);
        if ($data !== false) {
            return json_decode($data, true);
        }
    }
    
    // Генерируем новые данные
    $data = $callback();
    $redis->setex($key, $ttl, json_encode($data));
    $redis->hSet($tagVersionKey, $key, $combinedVersion);
    return $data;
}

// Пример использования
$users = getCachedData('users:list', ['user:update'], 300, function() {
    // Тяжёлый запрос к БД
    return ['user1', 'user2'];
});

// Инвалидация всех кэшей, связанных с тегом 'user:update'
$redis->incr('tag:user:update'); // увеличиваем версию тега

Проблема: избыточное количество ключей для тегов. Решение: использовать множество ключей для хранения связки кэш-теги или Redis Sets. Производительность может снижаться при большом количестве кэшей на один тег.

Расширение Redis для PHP - comments

En
Php redis extension (php)