Кэширование аутентифицированных пользователей в Laravel

Источник: «Caching Authenticated Users in Laravel»
Для ускорения работы приложений Laravel с высокой посещаемостью можно кэшировать аутентифицированных пользователей, избавляясь от лишних обращений к базе данных.

Я покажу, как это сделать, но это не быстрое решение; придётся подумать, что произойдёт, когда пользователь будет обновлён или удалён.

Давайте закэшируем.

Зачем кэшировать аутентифицированных пользователей

Для каждой аутентифицированной страницы или запроса API в приложении Laravel извлекает пользователя из базы данных с помощью запроса, аналогичного этому (в зависимости от ID, конечно):

select * from `users` where `id` = 1 limit 1

На текущий момент не существует способа автоматического кэширования этого пользовательского объекта. Таким образом, пока пользователь аутентифицирован, этот запрос к базе данных будет выполняться при каждом запросе пользователя.

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

Как работают провайдеры авторизации в Laravel

Для начала необходимо разобраться с механизмом провайдера Auth в Laravel.

По умолчанию Laravel использует EloquentUserProvider для управления аутентифицированными пользователями. Этот класс содержит множество полезных методов, таких как retrieveById, rehashPasswordIfRequired и validateCredentials. В общем, всё, что нужно для получения и обновления пользователя в отношении аутентификации.

Можно добавить нового провайдера используя метод Auth::provider следующим образом:

Auth::provider('someCustomProvider', function (Application $app, array $config) {
// здесь возвращается кастомный провайдер
});

После добавления провайдера во время выполнения можно подключить его в файле конфигурации config/auth.php:

'providers' => [
'users' => [
'driver' => 'someCustomProvider',
'model' => env('AUTH_MODEL', App\Models\User::class),
],

Создание собственного провайдера аутентификации

Теперь мы немного лучше понимаем, что такое провайдеры авторизации, создадим собственный!

Начнём с создания класса провайдера, CachedEloquentUserProvider. Его можно назвать как угодно и поместить в любое место в приложении:

namespace App\Auth\Providers;

use Illuminate\Auth\EloquentUserProvider;

class CachedEloquentUserProvider extends EloquentUserProvider
{
public function retrieveById($identifier)
{
//
}
}

Он расширяет базовый EloquentUserProvider, обеспечивая всю дополнительную функциональность, необходимую нам. Нас интересует только переопределение retrieveById для выбора способа получения пользователя.

Сейчас мы ничего не делаем для получения пользователя (об этом речь пойдёт дальше), но давайте свяжем его с нашим провайдером в методе boot AppServerProvider'а:

public function boot(): void
{
Auth::provider('cachedEloquent', function (Application $app, array $config) {
return new CachedEloquentUserProvider(
$app['hash'],
$config['model']
);
});
}

В конструкторе оригинального EloquentUserProvider необходимо передать текущий хэшер (отвечающий за хэширование паролей и т.д.), а также пространство имён модели из конфига, представляющее User (обычно App\Models\User).

Вот поэтому мы и передали эти две вещи выше.

Теперь заменим драйвер в config/auth.php на cachedEloquent (или как вы его назвали).

'providers' => [
'users' => [
'driver' => 'cachedEloquent',
'model' => env('AUTH_MODEL', App\Models\User::class),
],

На данный момент приложение не сможет получать данные об аутентифицированных пользователях, поскольку новый метод retrieveById пока пуст.

Возвращение кэшированного объекта пользователя

Пришло время заполнить метод retrieveById кэшированной версией пользователя.

Вариантов множество, но есть один, с которого можно начать:

public function retrieveById($identifier)
{
return cache()->remember('user_' . $identifier, now()->addHours(2), function () use ($identifier) {
return parent::retrieveById($identifier);
});
}

$identifier — просто идентификатор пользователя, поэтому передаём его родительскому методу retrieveById, чтобы он сделал своё дело. Но, конечно, с помощью cache()->remember кэшируем и возвращаем результат.

Вот более короткий способ получить тот же результат с помощью стрелочной функции PHP:

public function retrieveById($identifier)
{
return cache()->remember(
'user_' . $identifier,
now()->addHours(2),
fn () => parent::retrieveById($identifier)
);
}

Любой из этих методов работает хорошо; всё зависит от того, много ли других действий вы выполняете внутри замыкания, в этом случае лучше выбрать стандартную функцию обратного вызова.

Смените драйвер кэша

По умолчанию Laravel использует базу данных в качестве драйвера кэша. Необходимо изменить это, иначе вернёмся к получению кэшированной версии пользователя из базы данных… снова.

CACHE_STORE=redis

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

Сбрасывайте кэш, при изменениях

Кэширование — это здорово, но необходимо позаботиться о сбросе (инвалидации) кэша при изменениях.

Если пользователь прямо сейчас обновит свои данные в приложении, он не увидит немедленного отражения изменений и будет вынужден ждать, пока истечёт срок действия кэша. Не лучший вариант.

Чтобы избежать этого, достаточно наблюдать за изменениями User и сбрасывать кэш вручную.

Создадим UserObserver:

php artisan make:observer UserObserver

Откроем его и зарегистрируем события для обновления и удаления.

class UserObserver
{
public function updated(User $user)
{
cache()->forget('user_' . $user->id);
}

public function deleted(User $user)
{
cache()->forget('user_' . $user->id);
}
}

Ключ кэша был установлен на user_[id], поэтому просто сбрасываем этот ключ.

Регистрируем наблюдателя в модели User, и всё готово:

use App\Observers\UserObserver;

#[ObservedBy(UserObserver::class)]
class User extends Authenticatable
{
//...
}

Когда пользователи изменяются или удаляются, кэш становится неактуальным, и в итоге кэшируются свежие данные (или полностью удаляются, при удалении пользователя).

Кэширование — это сложно

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

Например:

Как говорилось в начале статьи, это не быстрое и лёгкое решение, не требующее обдумывания.

Да, это ускорит работу приложения. Но придётся приложить немного больше усилий, чтобы убедиться, что кэш не возвращает устаревшие данные или что кэш не сбрасывается слишком часто, делая аутентифицированное кэширование бесполезным.

Комментарии


Дополнительные материалы

Предыдущая Статья

Различие между PHP getenv() и $_ENV