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 协议》,转载必须注明作者和本文链接
本帖由系统于 5年前 自动加精
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 9

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

5年前 评论

分享一下我关于 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:

5年前 评论
BestKind (作者) 4年前
mengdodo 4年前

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

5年前 评论
L学习不停 5年前
Colorado 5年前
aen233 5年前

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

5年前 评论

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

5年前 评论
BestKind 5年前

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

5年前 评论

Syntax error or access violation: 1071 Specified key was too long; max key length is 1000 bytes 运行 php artisan migrate 报错

4年前 评论