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默认登陆界面.png

安装laravel-debugbar,观察访问中所进行的database query的数量。

备注:laravel-debugbar是一个非常好用的工具,可以观察到目前使用了多少个界面、访问多少次数据库,指令具体是什么,耗费时间是多少,在优化时经常使用。这个包强烈建议只安装在dev环境,否则会有泄漏敏感数据的危险。

$ composer require barryvdh/laravel-debugbar --dev

安装后,我们会发现,页面下方出现红色的debug条目,点开可以查看当前页面query数量。在登录状态下,用户访问任何界面,都会显示database query数量为1,访问了users表。目前这个访问是比较快的。但当users表增大时,这个数字会显著增加。

laravel debuglar显示,登陆后数据库query数为1

安装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,但不影响各种访问。

缓存后,数据库query数为0.png

如何将它适配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阶段之后实际优化效果是什么。

参考

网络上搜了一下缺少类似的内容,新写的教程,恳请指正。
第一次在这里发文,不确定格式是否正确,希望大家喜欢。

20200312更新
修改了observer,将updated之后保存user model,修改为saved之后清空原本缓存的user model。进行修改的原因,是因为,直接在updated之后保存user model,里面original和attributes的内容仍含有修改的状态,可能会影响后续的处理(短时间内递增和递减相同的值,可能会导致修改的内容不能保存)。将其缓存的内容清零,在下次需要的时候直接从数据库获取最新变量,能够彻底避免这个问题。

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 1个月前 自动加精
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 8

学习了, 根据查阅了资料和参考楼主的做法, 我做成了一个composer包,直接可以无损安装与更改,
第一次发布博客,很多东西都不熟悉,没有引用楼主的参考资料在上面,望见谅,如有问题请指正

1个月前 评论

分享一下我关于 passportuser cache 方案。

passportcache user 重要的点在于要同时 cache usertoken (client 因为不会很多可以不用 cache); 其中 user 的部分,楼主的方式就可以了,我来说说我关于 token 的实现

  1. 独立实现一个 tokencache 和查询类 PassportTokenModelCacheProvider
  2. AuthServiceProviderboot 中重写 token modelPassport::useTokenModel(PassportTokenCacheModelProvider::class);

对于以上实现进行说明:

passport 实现验证用户的地方关键在于 passportTokenGuard 类的 authenticateViaBearerToken 方法

有兴趣的同学可以仔细看看这个方法,重点可以自行断点查看这个方法中关于查询 usertoken 的那两个地方。如有错误的地方,欢迎大家斧正 :blush:

1个月前 评论

建议不要直接把对象放到cache,可以toArray后放入,取出再转换成对象。

1个月前 评论
L学习不停 3周前
Colorado 3周前
aen233 1周前

20200312 更新
修改了 observer,将 updated 之后保存 user model,修改为 saved 之后清空原本缓存的 user model。进行修改的原因,是因为,直接在 updated 之后保存 user model,里面 original 和 attributes 的内容仍含有修改的状态,可能会影响后续的处理(短时间内递增和递减相同的值,可能会导致修改的内容不能保存)。将其缓存的内容清零,在下次需要的时候直接从数据库获取最新变量,能够彻底避免这个问题。
(当然,如果养成良好的习惯,只要涉及保存就不使用缓存,也可以避免这个问题。不过我觉得还是这样保险一点)

3周前 评论

如果我的数据库是读写分离,passport刚分配给我的token,再次使用返回无效,这种情况应该怎么处理?

3周前 评论
BestKind 3周前

一般情况 会在jwt claim 里塞相关user的信息 而不是查询数据库,jwt rfc里有相关的东西

3周前 评论

请勿发布不友善或者负能量的内容。与人为善,比聪明更重要!