Redis -- высокопроизводительное хранилище данных в памяти, используемое в Laravel для кэширования, очередей, сессий, broadcasting и rate limiting. Laravel предоставляет удобный интерфейс для работы с Redis через facade Redis.
Подключение к Redis
Конфигурация
// config/database.php
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'), // phpredis or predis
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', \Illuminate\Support\Str::slug(
env('APP_NAME', 'laravel'), '_') . '_database_'
),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
],
'queue' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_QUEUE_DB', '2'),
],
'session' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_SESSION_DB', '3'),
],
],
Клиенты Redis
phpredis (рекомендуется) -- C-расширение, высокая производительность.
# Install phpredis extension
pecl install redis
predis -- PHP-пакет, не требует расширения.
composer require predis/predis
Кластеры Redis
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'clusters' => [
'default' => [
[
'host' => env('REDIS_HOST', 'localhost'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
],
],
],
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
],
],
Основные команды Redis
use Illuminate\Support\Facades\Redis;
// ============ Строки (Strings) ============
// Set a value
Redis::set('key', 'value');
// Set with expiration (seconds)
Redis::setex('key', 3600, 'value');
// Set only if key does NOT exist
Redis::setnx('key', 'value');
// Get a value
$value = Redis::get('key');
// Delete a key
Redis::del('key');
Redis::del(['key1', 'key2', 'key3']);
// Check if key exists
$exists = Redis::exists('key');
// Set expiration
Redis::expire('key', 3600); // In seconds
Redis::expireat('key', time() + 3600); // Unix timestamp
// Get remaining TTL
$ttl = Redis::ttl('key');
// Increment / Decrement
Redis::incr('counter');
Redis::incrby('counter', 5);
Redis::decr('counter');
Redis::decrby('counter', 5);
Redis::incrbyfloat('balance', 1.5);
// Multiple get/set
Redis::mset(['key1' => 'val1', 'key2' => 'val2']);
$values = Redis::mget(['key1', 'key2']);
Работа с хешами (Hashes)
use Illuminate\Support\Facades\Redis;
// Set hash fields
Redis::hset('user:1', 'name', 'John');
Redis::hset('user:1', 'email', '[email protected]');
// Set multiple hash fields
Redis::hmset('user:1', [
'name' => 'John',
'email' => '[email protected]',
'age' => 30,
]);
// Get a hash field
$name = Redis::hget('user:1', 'name');
// Get multiple hash fields
$data = Redis::hmget('user:1', ['name', 'email']);
// Get all hash fields
$user = Redis::hgetall('user:1');
// Returns: ['name' => 'John', 'email' => '[email protected]', 'age' => '30']
// Check if field exists
$exists = Redis::hexists('user:1', 'name');
// Delete a field
Redis::hdel('user:1', 'age');
// Increment hash field
Redis::hincrby('user:1', 'login_count', 1);
Работа со списками (Lists)
use Illuminate\Support\Facades\Redis;
// Push to the right (end)
Redis::rpush('queue:emails', 'email1');
Redis::rpush('queue:emails', 'email2', 'email3');
// Push to the left (beginning)
Redis::lpush('queue:emails', 'email0');
// Pop from the left (FIFO)
$email = Redis::lpop('queue:emails');
// Pop from the right (LIFO)
$email = Redis::rpop('queue:emails');
// Blocking pop (wait for an element)
$result = Redis::blpop(['queue:emails'], 10); // Wait up to 10 seconds
// Get list length
$length = Redis::llen('queue:emails');
// Get element by index
$element = Redis::lindex('queue:emails', 0);
// Get range of elements
$elements = Redis::lrange('queue:emails', 0, -1); // All elements
// Trim list
Redis::ltrim('queue:emails', 0, 99); // Keep only first 100
Работа с множествами (Sets)
use Illuminate\Support\Facades\Redis;
// Add members
Redis::sadd('tags:post:1', 'php', 'laravel', 'redis');
// Check membership
$isMember = Redis::sismember('tags:post:1', 'php'); // true
// Get all members
$tags = Redis::smembers('tags:post:1');
// Remove a member
Redis::srem('tags:post:1', 'redis');
// Set operations
$union = Redis::sunion('tags:post:1', 'tags:post:2');
$intersect = Redis::sinter('tags:post:1', 'tags:post:2');
$diff = Redis::sdiff('tags:post:1', 'tags:post:2');
// Random member
$random = Redis::srandmember('tags:post:1');
// Set size
$count = Redis::scard('tags:post:1');
Работа с сортированными множествами (Sorted Sets)
use Illuminate\Support\Facades\Redis;
// Add members with scores
Redis::zadd('leaderboard', 100, 'player1');
Redis::zadd('leaderboard', 250, 'player2');
Redis::zadd('leaderboard', 150, 'player3');
// Get rank (0-based, ascending)
$rank = Redis::zrank('leaderboard', 'player2');
// Get reverse rank (descending)
$rank = Redis::zrevrank('leaderboard', 'player2'); // 0 (top)
// Get score
$score = Redis::zscore('leaderboard', 'player2'); // 250
// Get range by rank (ascending)
$top3 = Redis::zrange('leaderboard', 0, 2);
// Get range by rank (descending) with scores
$top3 = Redis::zrevrange('leaderboard', 0, 2, 'WITHSCORES');
// Get range by score
$highScorers = Redis::zrangebyscore('leaderboard', 100, 200);
// Increment score
Redis::zincrby('leaderboard', 50, 'player1'); // Now 150
// Remove member
Redis::zrem('leaderboard', 'player1');
// Count members in score range
$count = Redis::zcount('leaderboard', 100, 200);
// Set size
$total = Redis::zcard('leaderboard');
Использование конкретного соединения
use Illuminate\Support\Facades\Redis;
// Use a specific connection
$value = Redis::connection('cache')->get('key');
// Use the default connection
$value = Redis::connection()->get('key');
// Or simply:
$value = Redis::get('key');
Pub/Sub (Публикация/Подписка)
Публикация сообщений
use Illuminate\Support\Facades\Redis;
// Publish a message to a channel
Redis::publish('chat-room', json_encode([
'user' => 'John',
'message' => 'Hello everyone!',
'timestamp' => now()->toISOString(),
]));
Подписка на канал
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
final class SubscribeToChat extends Command
{
protected $signature = 'chat:subscribe';
protected $description = 'Subscribe to Redis chat channel';
public function handle(): void
{
$this->info('Subscribed to chat-room channel...');
// subscribe() blocks the process
Redis::subscribe(['chat-room'], function (string $message, string $channel) {
$data = json_decode($message, true);
$this->line("[{$channel}] {$data['user']}: {$data['message']}");
});
}
}
Pattern Subscribe
// Subscribe to multiple channels using a pattern
Redis::psubscribe(['chat-room.*'], function (string $message, string $channel) {
// Matches: chat-room.general, chat-room.vip, etc.
$this->line("[{$channel}] {$message}");
});
Важно: subscribe() и psubscribe() блокируют выполнение. Их нужно запускать в отдельном процессе (Artisan command, supervisor).
Lua-скрипты
Lua-скрипты выполняются атомарно на сервере Redis, что гарантирует целостность при конкурентном доступе.
use Illuminate\Support\Facades\Redis;
// Simple Lua script
$result = Redis::eval(
"return redis.call('get', KEYS[1])",
1, // Number of KEYS arguments
'mykey' // KEYS[1]
);
// Atomic increment with limit
$result = Redis::eval(<<<'LUA'
local current = tonumber(redis.call('get', KEYS[1]) or 0)
local limit = tonumber(ARGV[1])
if current >= limit then
return 0 -- Limit reached
end
redis.call('incr', KEYS[1])
return 1 -- Success
LUA, 1, 'api:counter', 100); // KEYS[1] = 'api:counter', ARGV[1] = 100
// Atomic "check and set" pattern
$result = Redis::eval(<<<'LUA'
local current = redis.call('get', KEYS[1])
if current == ARGV[1] then
redis.call('set', KEYS[1], ARGV[2])
return 1
end
return 0
LUA, 1, 'status', 'pending', 'processing');
// Rate limiter in Lua (atomic)
$allowed = Redis::eval(<<<'LUA'
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = tonumber(redis.call('get', key) or 0)
if current >= limit then
return 0
end
if current == 0 then
redis.call('set', key, 1, 'EX', window)
else
redis.call('incr', key)
end
return 1
LUA, 1, 'rate:user:1', 60, 60); // 60 requests per 60 seconds
Pipelines
Pipelines позволяют отправить несколько команд за один сетевой вызов, значительно уменьшая latency.
use Illuminate\Support\Facades\Redis;
// Pipeline: commands are sent in a single network roundtrip
$results = Redis::pipeline(function ($pipe) {
for ($i = 0; $i < 1000; $i++) {
$pipe->set("key:{$i}", "value:{$i}");
}
});
// Read multiple keys in one roundtrip
$results = Redis::pipeline(function ($pipe) {
$pipe->get('user:1:name');
$pipe->get('user:1:email');
$pipe->hgetall('user:1:settings');
$pipe->smembers('user:1:roles');
});
// $results[0] = name, $results[1] = email, etc.
Важно: Pipeline НЕ является транзакцией. Команды выполняются последовательно, но между ними могут выполниться команды других клиентов.
Транзакции (MULTI/EXEC)
Транзакции Redis гарантируют, что набор команд выполнится атомарно.
use Illuminate\Support\Facades\Redis;
// Transaction: all commands execute atomically
$results = Redis::transaction(function ($tx) {
$tx->set('key1', 'value1');
$tx->set('key2', 'value2');
$tx->incr('counter');
});
// Transaction with WATCH (optimistic locking)
Redis::watch('user:1:balance');
$balance = Redis::get('user:1:balance');
if ($balance >= 100) {
$results = Redis::transaction(function ($tx) use ($balance) {
$tx->set('user:1:balance', $balance - 100);
$tx->incr('user:1:purchases');
});
if ($results === null) {
// Transaction failed (key was modified by another client)
// Retry logic needed
}
}
Pipeline vs Transaction:
| Характеристика | Pipeline | Transaction |
|---|---|---|
| Атомарность | Нет | Да (MULTI/EXEC) |
| Сетевых вызовов | 1 | 1 |
| Другие клиенты | Могут выполнять между командами | Заблокированы до EXEC |
| Использование | Массовые операции без гарантий | Критичные атомарные операции |
Rate Limiting с Redis
Laravel предоставляет встроенный Rate Limiter на основе Redis.
Middleware Rate Limiting
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Global API rate limit
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
// Custom rate limit
RateLimiter::for('uploads', function (Request $request) {
return $request->user()->isPremium()
? Limit::none()
: Limit::perMinute(10)->by($request->user()->id);
});
// Multiple limits
RateLimiter::for('login', function (Request $request) {
return [
Limit::perMinute(5)->by($request->ip()),
Limit::perHour(30)->by($request->ip()),
];
});
// Sliding window rate limit
RateLimiter::for('api-v2', function (Request $request) {
return Limit::perMinute(100)
->by($request->user()?->id ?: $request->ip())
->response(function (Request $request, array $headers) {
return response()->json([
'message' => 'Слишком много запросов. Попробуйте позже.',
], 429)->withHeaders($headers);
});
});
}
}
Программный Rate Limiter
use Illuminate\Support\Facades\RateLimiter;
// Attempt an action with rate limiting
$executed = RateLimiter::attempt(
key: 'send-email:' . $user->id,
maxAttempts: 5,
callback: function () use ($user) {
// Send the email
},
decaySeconds: 60, // Reset after 60 seconds
);
if (! $executed) {
$seconds = RateLimiter::availableIn('send-email:' . $user->id);
return "Попробуйте через {$seconds} секунд.";
}
// Manual rate limit check
$key = 'api-call:' . $user->id;
if (RateLimiter::tooManyAttempts($key, maxAttempts: 10)) {
$seconds = RateLimiter::availableIn($key);
abort(429, "Попробуйте через {$seconds} секунд.");
}
RateLimiter::hit($key, decaySeconds: 60);
// Clear rate limiter
RateLimiter::clear('send-email:' . $user->id);
// Get remaining attempts
$remaining = RateLimiter::remaining('send-email:' . $user->id, 5);
Redis для кэширования
// config/cache.php
'stores' => [
'redis' => [
'driver' => 'redis',
'connection' => env('CACHE_STORE', 'cache'),
'lock_connection' => env('CACHE_LOCK_CONNECTION', 'default'),
],
],
use Illuminate\Support\Facades\Cache;
// Cache with Redis
Cache::store('redis')->put('key', 'value', now()->addHours(1));
$value = Cache::store('redis')->get('key');
// Remember (get or set)
$users = Cache::store('redis')->remember('users:active', 3600, function () {
return User::where('active', true)->get();
});
// Tags (Redis and Memcached only)
Cache::tags(['users', 'admins'])->put('admin:1', $admin, 3600);
Cache::tags(['users'])->flush(); // Flush all user-related cache
Атомарные блокировки (Atomic Locks)
use Illuminate\Support\Facades\Cache;
// Acquire a lock
$lock = Cache::lock('processing-order', 10); // 10 seconds
if ($lock->get()) {
try {
// Perform exclusive operation
} finally {
$lock->release();
}
}
// Block until lock is available
$lock = Cache::lock('processing-order', 10);
$lock->block(5, function () {
// Lock acquired, execute. Block up to 5 seconds.
});
// Using owner token for distributed locks
$lock = Cache::lock('deployment', 600);
if ($lock->get()) {
$owner = $lock->owner();
// Pass $owner to another process/server
// On another process:
Cache::restoreLock('deployment', $owner)->release();
}
Тестирование
<?php
declare(strict_types=1);
namespace Tests\Feature;
use Illuminate\Support\Facades\Redis;
use Tests\TestCase;
final class RedisTest extends TestCase
{
public function test_redis_caching(): void
{
Redis::flushdb(); // Clean test database
Redis::set('test:key', 'test:value');
$this->assertEquals('test:value', Redis::get('test:key'));
}
public function test_rate_limiting(): void
{
$user = \App\Models\User::factory()->create();
// Make 60 requests (limit)
for ($i = 0; $i < 60; $i++) {
$response = $this->actingAs($user)
->getJson('/api/data');
$response->assertOk();
}
// 61st request should be rate limited
$response = $this->actingAs($user)
->getJson('/api/data');
$response->assertStatus(429);
}
}