Laravel 效能優化實踐:在 Auth 中用 Cache 排程快取的 User 模型

lyn510發表於2020-01-29

原始碼地址

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-debuglar,觀察訪問中所進行的database query的數量。

備註:laravel-debuglar是一個非常好用的工具,可以觀察到目前使用了多少個介面、訪問多少次資料庫,指令具體是什麼,耗費時間是多少,在優化時經常使用。這個包強烈建議只安裝在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 updated(User $user) // whenever there's update of user, renew cached instance
    {
        Cache::put("cachedUser.{$user->id}", $user, 30);
    }
}

將這個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階段之後實際優化效果是什麼。

參考

網路上搜了一下缺少類似的內容,新寫的教程,懇請指正。
第一次在這裡發文,不確定格式是否正確,希望大家喜歡。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章