HardПрактика8 min

Кэширование

Драйверы кэша, cache tags, атомарные блокировки, Cache::remember, Cache::forever, очистка кэша в Laravel 11

Кэширование в Laravel

Кэширование - критически важный аспект производительности приложения. Laravel предоставляет унифицированный API для работы с различными бэкендами кэширования.

Конфигурация

// config/cache.php
return [
    'default' => env('CACHE_STORE', 'database'),

    'stores' => [
        'array' => [
            'driver' => 'array',
            'serialize' => false,
        ],

        'database' => [
            'driver' => 'database',
            'connection' => env('DB_CACHE_CONNECTION'),
            'table' => env('DB_CACHE_TABLE', 'cache'),
            'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
            'lock_table' => env('DB_CACHE_LOCK_TABLE'),
        ],

        'file' => [
            'driver' => 'file',
            'path' => storage_path('framework/cache/data'),
            'lock_path' => storage_path('framework/cache/data'),
        ],

        'memcached' => [
            'driver' => 'memcached',
            'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
            'servers' => [
                [
                    'host' => env('MEMCACHED_HOST', '127.0.0.1'),
                    'port' => env('MEMCACHED_PORT', 11211),
                    'weight' => 100,
                ],
            ],
        ],

        'redis' => [
            'driver' => 'redis',
            'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
            'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
        ],

        'dynamodb' => [
            'driver' => 'dynamodb',
            'key' => env('AWS_ACCESS_KEY_ID'),
            'secret' => env('AWS_SECRET_ACCESS_KEY'),
            'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
            'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
            'endpoint' => env('DYNAMODB_ENDPOINT'),
        ],

        'octane' => [
            'driver' => 'octane',
        ],
    ],

    'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'),
];

Основные операции

Получение данных

use Illuminate\Support\Facades\Cache;

// Get value with default
$value = Cache::get('key');
$value = Cache::get('key', 'default');
$value = Cache::get('key', fn () => $this->computeExpensiveValue());

// Check if key exists
if (Cache::has('key')) {
    // Key exists and is not null
}

if (Cache::missing('key')) {
    // Key does not exist
}

// Get multiple values
$values = Cache::many(['key1', 'key2', 'key3']);
// ['key1' => 'value1', 'key2' => null, 'key3' => 'value3']

// Get and delete
$value = Cache::pull('key');
$value = Cache::pull('key', 'default');

Сохранение данных

// Store with TTL (seconds or DateTime)
Cache::put('key', 'value', 3600);                    // 3600 seconds
Cache::put('key', 'value', now()->addHours(2));       // 2 hours
Cache::put('key', 'value', now()->addMinutes(30));    // 30 minutes

// Store multiple
Cache::putMany([
    'key1' => 'value1',
    'key2' => 'value2',
], 3600);

// Store forever (no expiration)
Cache::forever('key', 'value');

// Store if not exists (atomic)
$added = Cache::add('key', 'value', 3600);
// Returns true if stored, false if key already exists

// Increment / Decrement
Cache::increment('counter');
Cache::increment('counter', 5);
Cache::decrement('counter');
Cache::decrement('counter', 3);

Удаление данных

// Remove specific key
Cache::forget('key');

// Clear entire cache
Cache::flush();

Cache::remember и Cache::rememberForever

Самые часто используемые методы. Получают значение из кэша или вычисляют и сохраняют:

// Remember: get from cache or compute and store
$users = Cache::remember('users:active', 3600, function () {
    return User::where('is_active', true)
        ->with('roles')
        ->orderBy('name')
        ->get();
});

// Remember forever
$settings = Cache::rememberForever('app:settings', function () {
    return Setting::all()->pluck('value', 'key')->toArray();
});

// With dynamic key
$userPosts = Cache::remember(
    "users:{$userId}:posts:page:{$page}",
    now()->addMinutes(15),
    fn () => Post::where('user_id', $userId)
        ->latest()
        ->paginate(20, ['*'], 'page', $page)
);

Практический пример: сервис с кэшированием

declare(strict_types=1);

namespace App\Services;

use App\Models\Category;
use App\Models\Product;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Cache;

final class CatalogService
{
    private const CACHE_TTL = 3600; // 1 hour

    /**
     * Get category tree with caching.
     *
     * @return Collection<int, Category>
     */
    public function getCategoryTree(): Collection
    {
        return Cache::remember('catalog:categories:tree', self::CACHE_TTL, function () {
            return Category::with('children')
                ->whereNull('parent_id')
                ->orderBy('sort_order')
                ->get();
        });
    }

    /**
     * Get products for category with caching.
     *
     * @return Collection<int, Product>
     */
    public function getProductsByCategory(int $categoryId, string $sortBy = 'name'): Collection
    {
        $cacheKey = "catalog:category:{$categoryId}:products:sort:{$sortBy}";

        return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($categoryId, $sortBy) {
            return Product::where('category_id', $categoryId)
                ->where('is_published', true)
                ->orderBy($sortBy)
                ->get();
        });
    }

    /**
     * Clear catalog cache when data changes.
     */
    public function clearCatalogCache(?int $categoryId = null): void
    {
        if ($categoryId) {
            // Clear specific category cache
            Cache::forget("catalog:category:{$categoryId}:products:sort:name");
            Cache::forget("catalog:category:{$categoryId}:products:sort:price");
            Cache::forget("catalog:category:{$categoryId}:products:sort:created_at");
        }

        // Always clear category tree
        Cache::forget('catalog:categories:tree');
    }
}

Cache Tags

Cache tags позволяют группировать связанные записи и удалять их разом. Поддерживаются только redis и memcached драйверами.

// Store with tags
Cache::tags(['users', 'profiles'])->put("user:{$id}:profile", $profile, 3600);
Cache::tags(['users', 'posts'])->put("user:{$id}:posts", $posts, 3600);
Cache::tags(['posts'])->put("post:{$postId}", $post, 3600);

// Retrieve tagged cache
$profile = Cache::tags(['users', 'profiles'])->get("user:{$id}:profile");

// Remember with tags
$stats = Cache::tags(['users', 'stats'])->remember(
    "user:{$id}:stats",
    now()->addMinutes(30),
    fn () => $this->calculateUserStats($id)
);

// Flush ALL items with specific tag
Cache::tags(['users'])->flush();
// Removes: user:1:profile, user:1:posts, user:2:profile, etc.

Cache::tags(['posts'])->flush();
// Removes: user:1:posts, post:1, post:2, etc.
// But user:1:profile stays (it's not tagged with 'posts')

::alert{type="danger"} Критически важно для экзамена: Cache tags НЕ поддерживаются драйверами file, database и dynamodb. Использование tags с этими драйверами приведёт к исключению. Tags работают только с redis и memcached. ::

Практический пример с тегами

declare(strict_types=1);

namespace App\Services;

use App\Models\User;
use Illuminate\Support\Facades\Cache;

final class UserCacheService
{
    public function getUserProfile(int $userId): array
    {
        return Cache::tags(['users', "user:{$userId}"])->remember(
            "user:{$userId}:profile",
            3600,
            fn () => User::with(['roles', 'permissions'])->findOrFail($userId)->toArray()
        );
    }

    public function getUserOrders(int $userId): array
    {
        return Cache::tags(['users', "user:{$userId}", 'orders'])->remember(
            "user:{$userId}:orders",
            1800,
            fn () => Order::where('user_id', $userId)->latest()->limit(50)->get()->toArray()
        );
    }

    /**
     * Clear all cache for specific user.
     */
    public function clearUserCache(int $userId): void
    {
        Cache::tags(["user:{$userId}"])->flush();
    }

    /**
     * Clear all user-related cache.
     */
    public function clearAllUsersCache(): void
    {
        Cache::tags(['users'])->flush();
    }

    /**
     * Clear all order-related cache.
     */
    public function clearOrdersCache(): void
    {
        Cache::tags(['orders'])->flush();
    }
}

Атомарные блокировки (Atomic Locks)

Атомарные блокировки предотвращают race conditions при конкурентном доступе:

use Illuminate\Support\Facades\Cache;

// Acquire lock
$lock = Cache::lock('processing-order-' . $orderId, seconds: 10);

if ($lock->get()) {
    try {
        // Critical section - only one process at a time
        $this->processOrder($orderId);
    } finally {
        // ALWAYS release in finally block
        $lock->release();
    }
}

Block - ожидание блокировки

$lock = Cache::lock('report-generation', seconds: 120);

try {
    // Wait up to 10 seconds to acquire lock
    $lock->block(10);

    // Lock acquired - generate report
    $this->generateReport();
} catch (LockTimeoutException $e) {
    $this->error('Could not acquire lock - another report is being generated.');
} finally {
    $lock?->release();
}

Блокировка с callback

// Simplified syntax with callback
Cache::lock('order-processing', 10)->block(5, function () use ($order) {
    // Lock acquired, process order
    $this->processOrder($order);
    // Lock automatically released after callback
});

Владение блокировкой

// Get lock owner token
$lock = Cache::lock('deployment', 600);
$lock->get();
$ownerToken = $lock->owner();

// Store token (e.g., in job payload)
// Later, release lock using token from another process
Cache::restoreLock('deployment', $ownerToken)->release();

Практический пример: предотвращение дублирования платежей

declare(strict_types=1);

namespace App\Services;

use App\Exceptions\PaymentAlreadyProcessingException;
use App\Models\Order;
use Illuminate\Cache\LockTimeoutException;
use Illuminate\Support\Facades\Cache;

final class PaymentService
{
    public function processPayment(Order $order): PaymentResult
    {
        $lockKey = "payment:order:{$order->id}";
        $lock = Cache::lock($lockKey, seconds: 60);

        try {
            // Try to acquire lock, wait up to 5 seconds
            $lock->block(5);
        } catch (LockTimeoutException) {
            throw new PaymentAlreadyProcessingException(
                "Payment for order {$order->id} is already being processed."
            );
        }

        try {
            // Double-check order status after acquiring lock
            $order->refresh();
            if ($order->isPaid()) {
                return PaymentResult::alreadyPaid();
            }

            // Process payment
            $result = $this->gateway->charge($order);

            // Update order
            $order->update([
                'status' => 'paid',
                'paid_at' => now(),
                'payment_id' => $result->transactionId,
            ]);

            return $result;
        } finally {
            $lock->release();
        }
    }
}

Работа с конкретным кэш-хранилищем

// Use specific store
$value = Cache::store('redis')->get('key');
Cache::store('file')->put('key', 'value', 3600);

// Use different store for different data
$hotData = Cache::store('redis')->remember('hot:data', 60, fn () => /* ... */);
$coldData = Cache::store('file')->remember('cold:data', 86400, fn () => /* ... */);

Очистка кэша

# Clear all cache
php artisan cache:clear

# Clear specific store
php artisan cache:clear --store=redis

# Clear config cache
php artisan config:clear

# Clear route cache
php artisan route:clear

# Clear view cache
php artisan view:clear

# Clear compiled classes
php artisan clear-compiled

# Clear everything
php artisan optimize:clear

Кэширование конфигурации и маршрутов

# Cache configuration (MUST for production)
php artisan config:cache

# Cache routes (MUST for production)
php artisan route:cache

# Cache views (recommended for production)
php artisan view:cache

# Cache events (auto-discovery cache)
php artisan event:cache

# All-in-one optimization
php artisan optimize

::alert{type="warning"} Для экзамена: php artisan config:cache объединяет все конфигурационные файлы в один кэшированный файл. После кэширования env() функция возвращает null при вызове вне конфигурационных файлов. Все вызовы env() должны быть ТОЛЬКО в файлах config/*.php. ::

Cache Events

use Illuminate\Cache\Events\CacheHit;
use Illuminate\Cache\Events\CacheMissed;
use Illuminate\Cache\Events\KeyWritten;
use Illuminate\Cache\Events\KeyForgotten;

// Listen to cache events
Event::listen(CacheHit::class, function (CacheHit $event) {
    Log::debug('Cache hit', ['key' => $event->key]);
});

Event::listen(CacheMissed::class, function (CacheMissed $event) {
    Log::debug('Cache miss', ['key' => $event->key]);
});

Тестирование кэша

declare(strict_types=1);

namespace Tests\Feature;

use Illuminate\Support\Facades\Cache;
use Tests\TestCase;

final class CatalogServiceTest extends TestCase
{
    public function test_categories_are_cached(): void
    {
        Category::factory()->count(5)->create();

        // First call - cache miss
        $service = app(CatalogService::class);
        $categories = $service->getCategoryTree();

        $this->assertCount(5, $categories);

        // Verify data is in cache
        $this->assertTrue(Cache::has('catalog:categories:tree'));

        // Delete from DB - cached data should still be returned
        Category::query()->delete();
        $cached = $service->getCategoryTree();

        $this->assertCount(5, $cached);
    }

    public function test_cache_is_cleared_on_update(): void
    {
        Cache::put('catalog:categories:tree', 'old_data', 3600);

        $service = app(CatalogService::class);
        $service->clearCatalogCache();

        $this->assertFalse(Cache::has('catalog:categories:tree'));
    }

    public function test_atomic_lock_prevents_duplicate(): void
    {
        $lock = Cache::lock('test-lock', 10);
        $lock->get();

        // Second attempt should fail
        $secondLock = Cache::lock('test-lock', 10);
        $this->assertFalse($secondLock->get());

        $lock->release();
    }
}

Проверь себя

Что делает метод Cache::add()?

Чем Cache::remember() отличается от Cache::get() с последующим Cache::put()?

Почему важно использовать finally при работе с атомарными блокировками?

Что произойдёт с вызовами env() вне файлов config/ после выполнения php artisan config:cache?

Какие кэш-драйверы поддерживают Cache Tags?