Изучаем расширение php-redis: кэширование и очереди в PHP
Расширение Redis для PHP: установка и варианты использования
Основное эффективное решение
Наиболее распространённый способ установки расширения php-redis - через PECL. После установки создаётся экземпляр класса Redis и выполняются базовые операции.
sudo pecl install redisPhp 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. Производительность может снижаться при большом количестве кэшей на один тег.