- Введение
- Настройка
- Использование кэша
- Атомарные блокировки
- Добавление пользовательских драйверов кэша
- События
#Введение
Некоторые задачи по получению или обработке данных в вашем приложении могут быть ресурсоёмкими для процессора или занимать несколько секунд. В таких случаях обычно данные кэшируются на некоторое время, чтобы при повторных запросах к тем же данным их можно было получить быстрее. Кэшированные данные обычно хранятся в очень быстром хранилище, таком как Memcached или Redis.
К счастью, Laravel предоставляет выразительный и единый API для различных кэш-бэкендов, позволяя использовать их высокую скорость получения данных и ускорять работу вашего веб-приложения.
#Настройка
Файл конфигурации кэша вашего приложения находится в config/cache.php. В этом файле вы можете указать, какой драйвер кэша будет использоваться по умолчанию во всём приложении. Laravel из коробки поддерживает популярные кэш-бэкенды, такие как Memcached, Redis, DynamoDB и реляционные базы данных. Кроме того, доступен файловый драйвер кэша, а драйверы array и "null" предоставляют удобные варианты кэша для автоматизированных тестов.
Файл конфигурации кэша также содержит различные другие параметры, которые документированы внутри файла, поэтому обязательно ознакомьтесь с ними. По умолчанию Laravel настроен на использование драйвера file, который сохраняет сериализованные объекты кэша в файловой системе сервера. Для крупных приложений рекомендуется использовать более надёжные драйверы, такие как Memcached или Redis. Вы также можете настроить несколько конфигураций кэша для одного и того же драйвера.
#Требования к драйверу
#Database
При использовании драйвера кэша database необходимо создать таблицу для хранения элементов кэша. Ниже приведён пример объявления схемы Schema для такой таблицы:
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->unique();
$table->text('value');
$table->integer('expiration');
});
Вы также можете использовать Artisan-команду php artisan cache:table для генерации миграции с правильной схемой.
#Memcached
Для использования драйвера Memcached необходимо установить PECL-пакет Memcached. Все ваши серверы Memcached можно перечислить в файле конфигурации config/cache.php. В этом файле уже есть запись memcached.servers для начала работы:
'memcached' => [
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
При необходимости вы можете указать опцию host как путь к UNIX-сокету. В этом случае опция port должна быть установлена в 0:
'memcached' => [
[
'host' => '/var/run/memcached/memcached.sock',
'port' => 0,
'weight' => 100
],
],
#Redis
Перед использованием Redis в Laravel необходимо либо установить расширение PhpRedis через PECL, либо установить пакет predis/predis (~1.0) через Composer. Laravel Sail уже включает это расширение. Кроме того, официальные платформы развертывания Laravel, такие как Laravel Forge и Laravel Vapor, по умолчанию имеют установленное расширение PhpRedis.
Для получения дополнительной информации о настройке Redis обратитесь к его странице документации Laravel.
#DynamoDB
Перед использованием драйвера кэша DynamoDB необходимо создать таблицу DynamoDB для хранения всех кэшированных данных. Обычно эта таблица должна называться cache. Однако имя таблицы следует задавать в соответствии со значением параметра stores.dynamodb.table в конфигурации cache вашего приложения.
Эта таблица также должна иметь строковой ключ раздела с именем, соответствующим значению параметра stores.dynamodb.attributes.key в конфигурации cache вашего приложения. По умолчанию ключ раздела должен называться key.
#Использование кэша
#Получение экземпляра кэша
Для получения экземпляра хранилища кэша вы можете использовать фасад Cache, который будет использоваться в этой документации. Фасад Cache предоставляет удобный и лаконичный доступ к внутренним реализациям контрактов кэша Laravel:
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Cache;
class UserController extends Controller
{
/**
* Показать список всех пользователей приложения.
*/
public function index(): array
{
$value = Cache::get('key');
return [
// ...
];
}
}
#Доступ к нескольким хранилищам кэша
Используя фасад Cache, вы можете получить доступ к разным хранилищам кэша через метод store. Ключ, передаваемый в метод store, должен соответствовать одному из хранилищ, перечисленных в массиве stores в файле конфигурации cache:
$value = Cache::store('file')->get('foo');
Cache::store('redis')->put('bar', 'baz', 600); // 10 минут
#Получение элементов из кэша
Метод get фасада Cache используется для получения элементов из кэша. Если элемент отсутствует в кэше, будет возвращено null. При желании вы можете передать вторым аргументом методу get значение по умолчанию, которое будет возвращено, если элемент не существует:
$value = Cache::get('key');
$value = Cache::get('key', 'default');
Вы также можете передать замыкание в качестве значения по умолчанию. Результат выполнения замыкания будет возвращён, если указанный элемент отсутствует в кэше. Это позволяет отложить получение значений по умолчанию из базы данных или другого внешнего сервиса:
$value = Cache::get('key', function () {
return DB::table(/* ... */)->get();
});
#Проверка существования элемента
Метод has позволяет проверить, существует ли элемент в кэше. Этот метод также вернёт false, если элемент существует, но его значение равно null:
if (Cache::has('key')) {
// ...
}
#Увеличение / уменьшение значений
Методы increment и decrement позволяют изменять значение целочисленных элементов в кэше. Оба метода принимают необязательный второй аргумент — величину, на которую нужно увеличить или уменьшить значение:
// Инициализировать значение, если оно отсутствует...
Cache::add('key', 0, now()->addHours(4));
// Увеличить или уменьшить значение...
Cache::increment('key');
Cache::increment('key', $amount);
Cache::decrement('key');
Cache::decrement('key', $amount);
#Получить и сохранить
Иногда нужно получить элемент из кэша, а если он отсутствует — сохранить значение по умолчанию. Например, можно получить всех пользователей из кэша или, если их там нет, загрузить из базы и добавить в кэш. Для этого используется метод Cache::remember:
$value = Cache::remember('users', $seconds, function () {
return DB::table('users')->get();
});
Если элемент отсутствует в кэше, замыкание, переданное методу remember, будет выполнено, и его результат будет помещён в кэш.
Метод rememberForever позволяет получить элемент из кэша или сохранить его навсегда, если он отсутствует:
$value = Cache::rememberForever('users', function () {
return DB::table('users')->get();
});
#Получить и удалить
Если нужно получить элемент из кэша и сразу удалить его, используйте метод pull. Как и метод get, он вернёт null, если элемент отсутствует:
$value = Cache::pull('key');
#Сохранение элементов в кэш
Для сохранения элементов в кэш используйте метод put фасада Cache:
Cache::put('key', 'value', $seconds = 10);
Если время хранения не передано в метод put, элемент будет храниться бессрочно:
Cache::put('key', 'value');
Вместо передачи количества секунд в виде целого числа, можно передать объект DateTime, указывающий время истечения срока действия кэшированного элемента:
Cache::put('key', 'value', now()->addMinutes(10));
#Сохранить, если отсутствует
Метод add добавит элемент в кэш только если его там ещё нет. Метод вернёт true, если элемент был добавлен, и false в противном случае. Операция add является атомарной:
Cache::add('key', 'value', $seconds);
#Сохранение элементов навсегда
Метод forever позволяет сохранить элемент в кэше навсегда. Поскольку такие элементы не истекают, их нужно удалять вручную с помощью метода forget:
Cache::forever('key', 'value');
Если вы используете драйвер Memcached, элементы, сохранённые "навсегда", могут быть удалены при достижении лимита размера кэша.
#Удаление элементов из кэша
Для удаления элементов из кэша используйте метод forget:
Cache::forget('key');
Также можно удалить элементы, указав время хранения в секундах равным нулю или отрицательному числу:
Cache::put('key', 'value', 0);
Cache::put('key', 'value', -5);
Для очистки всего кэша используйте метод flush:
Cache::flush();
Очистка кэша игнорирует настроенный вами префикс кэша "prefix" и удалит все записи из кэша. Учитывайте это при очистке кэша, который используется совместно с другими приложениями.
#Хелпер Cache
Помимо фасада Cache, вы можете использовать глобальную функцию cache для получения и сохранения данных в кэше. Если вызвать функцию cache с одним строковым аргументом, она вернёт значение по ключу:
$value = cache('key');
Если передать массив пар ключ/значение и время хранения, функция сохранит значения в кэше на указанное время:
cache(['key' => 'value'], $seconds);
cache(['key' => 'value'], now()->addMinutes(10));
Если вызвать функцию cache без аргументов, она вернёт экземпляр реализации Illuminate\Contracts\Cache\Factory, позволяя вызывать другие методы кэширования:
cache()->remember('users', $seconds, function () {
return DB::table('users')->get();
});
При тестировании вызовов глобальной функции cache вы можете использовать метод Cache::shouldReceive так же, как при тестировании фасада.
#Атомарные блокировки
Для использования этой функции ваше приложение должно использовать один из драйверов кэша memcached, redis, dynamodb, database, file или array в качестве драйвера кэша по умолчанию. Кроме того, все серверы должны работать с одним и тем же центральным сервером кэша.
#Требования к драйверу
#Database
При использовании драйвера кэша database необходимо создать таблицу для хранения блокировок кэша вашего приложения. Ниже приведён пример объявления схемы Schema для такой таблицы:
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration');
});
Если вы использовали Artisan-команду cache:table для создания таблицы кэша драйвера базы данных, миграция, созданная этой командой, уже содержит определение таблицы cache_locks.
#Управление блокировками
Атомарные блокировки позволяют управлять распределёнными блокировками без риска гонок. Например, Laravel Forge использует атомарные блокировки, чтобы гарантировать выполнение только одной удалённой задачи на сервере одновременно. Вы можете создавать и управлять блокировками с помощью метода Cache::lock:
use Illuminate\Support\Facades\Cache;
$lock = Cache::lock('foo', 10);
if ($lock->get()) {
// Блокировка получена на 10 секунд...
$lock->release();
}
Метод get также принимает замыкание. После выполнения замыкания Laravel автоматически снимет блокировку:
Cache::lock('foo', 10)->get(function () {
// Блокировка получена на 10 секунд и автоматически снята...
});
Если блокировка недоступна в момент запроса, вы можете указать Laravel ждать заданное количество секунд. Если блокировку не удастся получить за это время, будет выброшено исключение Illuminate\Contracts\Cache\LockTimeoutException:
use Illuminate\Contracts\Cache\LockTimeoutException;
$lock = Cache::lock('foo', 10);
try {
$lock->block(5);
// Блокировка получена после ожидания максимум 5 секунд...
} catch (LockTimeoutException $e) {
// Не удалось получить блокировку...
} finally {
$lock?->release();
}
Пример выше можно упростить, передав замыкание в метод block. В этом случае Laravel попытается получить блокировку в течение указанного времени и автоматически снимет её после выполнения замыкания:
Cache::lock('foo', 10)->block(5, function () {
// Блокировка получена после ожидания максимум 5 секунд...
});
#Управление блокировками между процессами
Иногда нужно получить блокировку в одном процессе и снять её в другом. Например, можно получить блокировку во время веб-запроса и снять её в конце очередной задачи, вызванной этим запросом. В таком случае следует передать токен владельца блокировки в очередь, чтобы задача могла восстановить блокировку по этому токену.
В примере ниже мы отправляем задачу в очередь, если блокировка успешно получена. Также мы передаём токен владельца блокировки в задачу через метод owner блокировки:
$podcast = Podcast::find($id);
$lock = Cache::lock('processing', 120);
if ($lock->get()) {
ProcessPodcast::dispatch($podcast, $lock->owner());
}
В задаче ProcessPodcast нашего приложения мы можем восстановить и снять блокировку, используя токен владельца:
Cache::restoreLock('processing', $this->owner)->release();
Если нужно снять блокировку без учёта текущего владельца, используйте метод forceRelease:
Cache::lock('processing')->forceRelease();
#Добавление пользовательских драйверов кэша
#Создание драйвера
Для создания собственного драйвера кэша сначала необходимо реализовать контракт Illuminate\Contracts\Cache\Store contract. Например, реализация кэша для MongoDB может выглядеть так:
<?php
namespace App\Extensions;
use Illuminate\Contracts\Cache\Store;
class MongoStore implements Store
{
public function get($key) {}
public function many(array $keys) {}
public function put($key, $value, $seconds) {}
public function putMany(array $values, $seconds) {}
public function increment($key, $value = 1) {}
public function decrement($key, $value = 1) {}
public function forever($key, $value) {}
public function forget($key) {}
public function flush() {}
public function getPrefix() {}
}
Осталось реализовать каждый из этих методов, используя соединение с MongoDB. Для примера реализации посмотрите класс Illuminate\Cache\MemcachedStore в исходном коде Laravel. После завершения реализации можно зарегистрировать пользовательский драйвер, вызвав метод extend фасада Cache:
Cache::extend('mongo', function (Application $app) {
return Cache::repository(new MongoStore);
});
Если вы не знаете, куда поместить код пользовательского драйвера кэша, можно создать пространство имён Extensions внутри каталога app. Однако помните, что Laravel не навязывает жёсткую структуру приложения, и вы можете организовать его по своему усмотрению.
#Регистрация драйвера
Чтобы зарегистрировать пользовательский драйвер кэша в Laravel, мы используем метод extend фасада Cache. Поскольку другие сервис-провайдеры могут попытаться прочитать кэшированные значения в своём методе boot, мы зарегистрируем наш драйвер внутри booting-обратного вызова. Используя booting, мы гарантируем, что пользовательский драйвер будет зарегистрирован сразу до вызова boot у сервис-провайдеров приложения, но после вызова register у всех провайдеров. Мы зарегистрируем наш booting-обратный вызов в методе register класса App\Providers\AppServiceProvider приложения:
<?php
namespace App\Providers;
use App\Extensions\MongoStore;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Регистрация сервисов приложения.
*/
public function register(): void
{
$this->app->booting(function () {
Cache::extend('mongo', function (Application $app) {
return Cache::repository(new MongoStore);
});
});
}
/**
* Загрузка сервисов приложения.
*/
public function boot(): void
{
// ...
}
}
Первый аргумент метода extend — имя драйвера. Оно должно совпадать с опцией driver в файле конфигурации config/cache.php. Второй аргумент — замыкание, которое должно возвращать экземпляр Illuminate\Cache\Repository. В замыкание передаётся $app — экземпляр контейнера сервисов.
После регистрации расширения обновите опцию driver в файле конфигурации config/cache.php, указав имя вашего расширения.
#События
Чтобы выполнять код при каждой операции с кэшем, вы можете слушать события, генерируемые кэшем. Обычно слушатели событий размещают в классе App\Providers\EventServiceProvider вашего приложения:
use App\Listeners\LogCacheHit;
use App\Listeners\LogCacheMissed;
use App\Listeners\LogKeyForgotten;
use App\Listeners\LogKeyWritten;
use Illuminate\Cache\Events\CacheHit;
use Illuminate\Cache\Events\CacheMissed;
use Illuminate\Cache\Events\KeyForgotten;
use Illuminate\Cache\Events\KeyWritten;
/**
* Отображение слушателей событий для приложения.
*
* @var array
*/
protected $listen = [
CacheHit::class => [
LogCacheHit::class,
],
CacheMissed::class => [
LogCacheMissed::class,
],
KeyForgotten::class => [
LogKeyForgotten::class,
],
KeyWritten::class => [
LogKeyWritten::class,
],
];