Laravel 性能优化实践:在 Auth 中用 Cache 调度缓存的 User 模型
Laravel 性能优化实战:在 Auth 中用 Cache 调度缓存的 User 模型#
源代码地址#
https://github.com/lyn510/laravel-auth-use...
摘要#
在 laravel 系统上线运行后我们发现,用户的每一次访问,都需要向数据库请求,验证其身份。对于用户较多的线上程序,频繁访问 users 表,成为系统的一个性能门槛。
本文所述方法 采用 redis 缓存 auth 访问所需的 Eloquent User Model,又采取 observer 跟踪更新这个 model ,减少在登陆时对模型的频繁访问。本教程也含对 passport API 适配的情况。
实践验证,这能显著减少数据库负担,降低系统运维成本,改善用户访问体验。
内容目录:#
阅读前需求#
- 本教程默认用户已安装 composer
- 本教程默认用户已为本地环境配置 mysql
- 本教程默认用户已为本地环境配置 redis,包括配置 predis 和安装 redis-cli。
- 本教程默认用户已有基础的 laravel 经验,知道如何本地 serve 程序,怎样在页面中打开自己的 laravel 工程。
前言#
随着网站用户的增加,运维负担也不断加大。因为 laravel 默认的 auth 方法会在每次访问时对数据库进行请求来获取 user model,对 users 表的频繁访问成为了网站性能的一个瓶颈。
一个常用的办法,是通过 cache,对 user model 进行缓存,避免在每次打开页面的时候,都访问一次数据库的 users 表格,有效减少 database query。
在 laravel 讨论社区,对这个方法进行实践的教程非常少。一个比较有用的教程来自 https://paulund.co.uk/laravel-cache-authus... ,但实践发现这个教程遗漏了通过 token 获取缓存 model 的办法,导致如果单纯依靠它,并不能在日常使用中成功缓存 user model。
实践中,我们在上述教程的基础上做了一些延伸,增加了通过 token 获取缓存 model 的办法。另外,增加了将这个办法拓展至 passport 配置 API 后端的部分。
我们将在这个问题上获得的一些经验分享,希望给更多的开发者朋友带来帮助。
为了方便理解,本教程将带领读者在全新空白 laravel 工程上,一步步配置。
内容或有疏漏,恳请指正。
正文#
在空白 laravel5.7 程序里,配置 auth、cache、database 等基本内容#
首先安装空白 laravel 5.7 教程
$ composer create-project --prefer-dist laravel/laravel laravel-auth-user "5.7.*"
安装 auth
$ php artisan make:auth
配置 mysql
在本地新建名为 laravel-auth-user
的 mysql 数据库,修改.env
文件,和本地 mysql 数据库进行连接:
...
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel-cache-auth
DB_USERNAME=root
DB_PASSWORD=
...
数据库迁移
$ php artisan migrate
serve 程序,打开页面尝试本地注册,顺利注册,可以登陆。
安装 laravel-debugbar,观察访问中所进行的 database query 的数量。
备注:laravel-debugbar 是一个非常好用的工具,可以观察到目前使用了多少个界面、访问多少次数据库,指令具体是什么,耗费时间是多少,在优化时经常使用。这个包强烈建议只安装在 dev 环境,否则会有泄漏敏感数据的危险。
$ composer require barryvdh/laravel-debugbar --dev
安装后,我们会发现,页面下方出现红色的 debug 条目,点开可以查看当前页面 query 数量。在登录状态下,用户访问任何界面,都会显示 database query 数量为 1,访问了 users 表。目前这个访问是比较快的。但当 users 表增大时,这个数字会显著增加。
安装 predis
$ composer require predis/predis
配置 redis,修改.env
文件
...
CACHE_DRIVER=redis
CACHE_PREFIX=laravel-cache-user
...
因为目前程序并没有使用 cache 的地方,为了直接测试是否关联成功,我们后台登陆查看。
$ php artisan tinker
在显示的界面中随便缓存一个内容
>>> Cache::put('data','success',1)
然后获取这个内容
>>> Cache::get('data')
应该会显示
>>> Cache::put('data','success',1)
=> null
>>> Cache::get('data')
=> "success"
>>>
这说明 cache 配置成功。
在上述工程基础上,增加 Cache-User 的具体办法#
在上面的内容中,将 user model 进行 cache。
首先,我们要将 User Model 缓存起来放到 cache 里,在需要的时候去调度它。
创建文件:app\Helpers\CacheUser.php
<?php
namespace App\Helpers;
use Cache;
use App\User;
use Auth;
class CacheUser{ //cache-user class
public static function user($id){
if(!$id||$id<=0||!is_numeric($id)){return;} // if $id is not a reasonable integer, return false instead of checking users table
return Cache::remember('cachedUser.'.$id, 30, function() use($id) {
return User::find($id); // cache user instance for 30 minutes
});
}
}
将这个 class 的简称加入列表,方便调用。修改 config\app.php
'aliases' => [
...
'CacheUser' => App\Helpers\CacheUser::class,
...
接着,我们需要确保,每当 UserModel 受到修改的时候,这个缓存的模型也会同步更新,避免内容失效。这是通过建立 observer 来实现的。建立文件 app\Observers\UserObserver.php
。
<?php
namespace App\Observers;
use Cache;
use App\User;
/**
* User observer
*/
class UserObserver
{
public function saved(User $user) // whenever there's update or create of user, renew cached instance
{
Cache::forget("cachedUser.{$user->id}");
}
}
将这个 observer 登记起来,让它自动运行。
修改 app\Providers\AppServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Observers\UserObserver;
use App\User;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
User::observe(UserObserver::class);
}
}
最后,我们需要让 Auth 方法在检查的时候,从缓存的 user 而非数据库 users 表来获取需要的模型
新建文件 app\Auth\CacheUserProvider.php
<?php
namespace App\Auth;
use App\User;
use Illuminate\Auth\EloquentUserProvider;
use Illuminate\Contracts\Hashing\Hasher as HasherContract;
use Illuminate\Support\Facades\Cache;
use CacheUser;
/**
* Class CacheUserProvider
* @package App\Auth
*/
class CacheUserProvider extends EloquentUserProvider
{
/**
* CacheUserProvider constructor.
* @param HasherContract $hasher
*/
public function __construct(HasherContract $hasher)
{
parent::__construct($hasher, User::class);
}
/**
* @param mixed $identifier
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveById($identifier)
{
return CacheUser::user($identifier);
}
public function retrieveByToken($identifier, $token)
{
$model = CacheUser::user($identifier);
if (! $model) {
return null;
}
$rememberToken = $model->getRememberToken();
return $rememberToken && hash_equals($rememberToken, $token) ? $model : null;
}
}
上面两个方法中,retrieveById
在用户输入用户名密码登录时调度。retrieveByToken
在用户后续登陆时通过 token 比对调度。两个方法的改写,保证用户登陆使用的是被缓存的用户模型。
为了将 CacheUserProvider 注册到 auth 中进行调用,修改文件 app\Providers\AuthServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use App\Auth\CacheUserProvider;
use Illuminate\Support\Facades\Auth;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
];
/**
* Register any authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();
Auth::provider('cache-user', function() {
return resolve(CacheUserProvider::class);
});
}
}
接着修改 config\auth.php
...
'providers' => [
'users' => [
'driver' => 'cache-user', // modify to use cached user instance
'model' => App\User::class,
],
...
serve 页面,登录状态下,刷新后可以发现,query 数量变成 0,但不影响各种访问。
如何将它适配 Passport#
最后讲一下如何适配 passport,实际上就是普通 passport 适配的基本教程,参见 laravel 官方文档。
$ composer require laravel/passport:^7.0
这一步注意,现行 passport 版本不支持 laravel5.7 框架,安装时需指定版本号。
$ php artisan migrate
$ php artisan passport:install
按照官方教程,add the Laravel\Passport\HasApiTokens trait to your App\User model
修改 app\User.php
<?php
namespace App;
use Laravel\Passport\HasApiTokens;
...
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
...
注册 api 路径
修改 app\Providers\AuthServiceProvider.php
<?php
namespace App\Providers;
use Laravel\Passport\Passport;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use App\Auth\CacheUserProvider;
use Illuminate\Support\Facades\Auth;
...
class AuthServiceProvider extends ServiceProvider
{
...
public function boot()
{
$this->registerPolicies();
Auth::provider('cache-user', function() {
return resolve(CacheUserProvider::class);
});
Passport::routes();
}
最后,修改 auth 默认的方式
修改文件 config\auth.php
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],
关于 api passport 中 cache user 的具体使用,我们还在摸索,暂时就说到这里。
结果#
在实际使用中,和本教程的区别在于,我们使用 redis 作为 session driver(这部分内容的配置参考官方教程即可)。在实际使用的前后端一体系统中(使用 blade 界面),增加 cache user 方法,能显著减轻对 user 表的负担,减少 mysql 数据库的负荷。目前我们的前后端分离系统仍在开发阶段,上述配置可行,但尚未来得及实践进入 passport API 阶段之后实际优化效果是什么。
参考#
- laravel 官方教程(5.7 版):https://learnku.com/docs/laravel/5.7
- 前人关于 cache user 的教程:https://paulund.co.uk/laravel-cache-authus...
网络上搜了一下缺少类似的内容,新写的教程,恳请指正。
第一次在这里发文,不确定格式是否正确,希望大家喜欢。
20200312 更新
修改了 observer,将 updated 之后保存 user model,修改为 saved 之后清空原本缓存的 user model。进行修改的原因,是因为,直接在 updated 之后保存 user model,里面 original 和 attributes 的内容仍含有修改的状态,可能会影响后续的处理(短时间内递增和递减相同的值,可能会导致修改的内容不能保存)。将其缓存的内容清零,在下次需要的时候直接从数据库获取最新变量,能够彻底避免这个问题。
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: