Laravel學習筆記之Redis保存頁面瀏覽量

說明:本文主要講述使用Redis做爲緩存加快頁面訪問速度。同時,做者會將開發過程當中的一些截圖和代碼黏上去,提升閱讀效率。php

備註:做者最近在學習github上別人的源碼時,發現好多在計算一篇博客頁面訪問量view_count時都是這麼作的:利用Laravel的事件監聽器監聽IP訪問該post,而後頁面每訪問一次,都刷新一次MySQL(假設MySQL)中post表的view_count字段,若是短期內大量的IP來訪問,那效率就不是很高了。何不用Redis來作緩存,等到該post達到必定瀏覽頁面後再刷新下MySQL,效率也很高。css

開發環境:Laravel5.1+MAMP+PHP7+MySQL5.5html

Redis依賴包安裝與配置

Redis就和MySQL同樣,都是數據庫,只不過MySQL是磁盤數據庫,數據存儲在磁盤裏,而Redis是內存數據庫,數據存儲在內存裏,不持久化的話服務器斷電數據就被抹掉了。Redis數據存儲類型比較多,包括:字符串類型哈希類型列表類型集合類型有序集合類型,而不像MySQL主要只有三類:字符串類型數字類型日期類型。Redis可做緩存系統、隊列系統。jquery

Redis服務端安裝

首先得在主機上裝下Redis服務端,以MAC爲例,Windows/Linux安裝也不少教程:laravel

brew install redis
//設置電腦啓動時也啓動redis-server
ln -sfv /usr/local/opt/redis/*.plist ~/Library/LaunchAgents
//經過launchctl啓動redis-server
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.redis.plist
//或者經過配置文件啓動
redis-server /usr/local/etc/redis.conf
//中止redis-server
launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.redis.plist
//卸載redis-server
$ brew uninstall redis
$ rm ~/Library/LaunchAgents/homebrew.mxcl.redis.plist
//測試是否安裝成功,出現pong,輸入redis-cli進入redis自帶的終端客戶端
redis-cli ping

主機安裝完,就能夠在Laravel環境安裝下PHP的Redis客戶端依賴包:git

composer require predis/predis

predis是用PHP語言寫的一個redis客戶端包,Laravel的Redis模塊依賴於這個包。
phpredis是C語言寫的一個PHP擴展,和predis功能差很少,只不過做爲擴展效率高些,phpredis能夠做爲擴展裝進PHP語言中,不過這裏沒用到,就不裝了。github

推薦Laravel開發插件三件套,提升開發效率,能夠參考做者寫的Laravel學習筆記之Seeder填充數據小技巧redis

composer require barryvdh/laravel-debugbar --dev
composer require barryvdh/laravel-ide-helper --dev
composer require mpociot/laravel-test-factory-helper --dev

//config/app.php
        /**
         *Develop Plugin
        */
        Barryvdh\Debugbar\ServiceProvider::class,
        Mpociot\LaravelTestFactoryHelper\TestFactoryHelperServiceProvider::class,
        Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,

配置下config/cache.php文件把緩存驅動設爲redis,還有redis自身配置在config/database.php文件中:數據庫

//config/cache.php
//'default' => 'redis',
'default' => env('CACHE_DRIVER', 'file'),//或者改下.env文件
'redis' => [
            'driver'     => 'redis',
            'connection' => 'default',//改成鏈接的實例,就默認鏈接'default'實例
        ],

//config/database.php
'redis' => [

        'cluster' => false,

        //就作一個實例,名爲'default'實例
        'default' => [
            'host'     => env('REDIS_HOST', 'localhost'),
            'password' => env('REDIS_PASSWORD', null),
            'port'     => env('REDIS_PORT', 6379),
            'database' => 0,
        ],

    ],

Redis存儲瀏覽量字段

先作個post表,建個post遷移文件再設計表字段值,包括seeder填充假數據,能夠參考下這篇文章Laravel學習筆記之Seeder填充數據小技巧,總之表字段以下:bootstrap

class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('category_id')->unsigned()->comment('外鍵');
            $table->string('title')->comment('標題');
            $table->string('slug')->unique()->index()->comment('錨點');
            $table->string('summary')->comment('概要');
            $table->text('content')->comment('內容');
            $table->text('origin')->comment('文章來源');
            $table->integer('comment_count')->unsigned()->comment('評論次數');
            $table->integer('view_count')->unsigned()->comment('瀏覽次數');
            $table->integer('favorite_count')->unsigned()->comment('點贊次數');
            $table->boolean('published')->comment('文章是否發佈');
            $table->timestamps();
            $table->foreign('category_id')
                  ->references('id')
                  ->on('categories')
                  ->onUpdate('cascade')
                  ->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('posts', function(Blueprint $tabel){
            $tabel->dropForeign('posts_category_id_foreign');
        });
        Schema::drop('posts');
    }
}

作一個控制器和一個路由:

php artisan make:controller PostController
Route::get('post/{id}', 'PostController@showPostCache');

利用Laravel的事件模塊,來定義一個IP訪問事件類,而後在事件監聽器類裏作一些邏輯處理如把訪問量存儲在Redis裏。Laravel的事件監聽這麼作:在EventServiceProvider裏定義事件和對應的監聽器,而後輸入指令:

//app/Providers/EventServiceProvider.php
protected $listen = [
        'App\Events\PostViewCount' => [
            'App\Listeners\PostEventListener',
        ],
       ] 

//指令
php artisan event:generate

在app/Event和app/Listeners會生成事件類和監聽器類。

在PostController寫上showPostCache方法:

const modelCacheExpires = 10;

public function showPostCache(Request $request, $id)
    {
        //Redis緩存中沒有該post,則從數據庫中取值,並存入Redis中,該鍵值key='post:cache'.$id生命時間10分鐘
        $post = Cache::remember('post:cache:'.$id, self::modelCacheExpires, function () use ($id) {
            return Post::whereId($id)->first();
        });

        //獲取客戶端IP
        $ip = $request->ip();
        //觸發瀏覽量計數器事件
        event(new PostViewCount($post, $ip));

        return view('browse.post', compact('post'));
    }

這裏Cache上文已經配置了以redis做爲驅動,這裏取IP,這樣防止同一IP短期內刷新頁面增長瀏覽量,event()或Event::fire()觸發事件,把$post和$ip做爲參數傳入,而後再定義事件類:

//app/Events/PostViewCount.php
/**
     * @var Post
     */
    public $post;

    /**
     * @var string
     */
    public $ip;

    /**
     * Create a new event instance.
     *
     * @param Post $post
     * @param string $ip
     */
    public function __construct(Post $post, $ip)
    {
        $this->post = $post;
        $this->ip   = $ip;
    }

順便也把視圖簡單寫下吧:

<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <!-- 上述3個meta標籤*必須*放在最前面,任何其餘內容都*必須*跟隨其後! -->
        <title>Bootstrap Template</title>
        <!-- 新 Bootstrap 核心 CSS 文件 -->
        <link rel="stylesheet" href="//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css">
        <style>
            html,body{
                width: 100%;
                height: 100%;
            }
            *{
                margin: 0;
                border: 0;
            }
            .jumbotron{
                margin-top: 10%;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <div class="row">
                <div class="col-xs-12 col-md-12">
                    <div class="jumbotron">
                        <h1>Title:{{$post->title}}</h1>
                        <span class="glyphicon glyphicon-eye-open" aria-hidden="true">{{$post->view_count}} views</span>
                        <h2>Summary:{{$post->summary}}</h2>
                        <p>Content:{{$post->content}}</p>
                    </div>
                </div>
            </div>
        </div>

        <!-- jQuery文件。務必在bootstrap.min.js 以前引入 -->
        <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
        <!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
        <script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
        <script>

        </script>
    </body>
</html>

而後重點寫下事件監聽器邏輯:

class PostEventListener
{
    /**
     * 同一post最大訪問次數,再刷新數據庫
     */
    const postViewLimit = 30;

    /**
     * 同一用戶瀏覽同一post過時時間
     */
    const ipExpireSec   = 300;

    /**
     * Create the event listener.
     *
     */
    public function __construct()
    {

    }

    /**
     * Handle the event.
     * 監聽用戶瀏覽事件
     * @param  PostViewCount  $event
     * @return void
     */
    public function handle(PostViewCount $event)
    {
        $post = $event->post;
        $ip   = $event->ip;
        $id   = $post->id;
        //首先判斷下ipExpireSec = 300秒時間內,同一IP訪問屢次,僅僅做爲1次訪問量
        if($this->ipViewLimit($id, $ip)){
            //一個IP在300秒時間內訪問第一次時,刷新下該篇post的瀏覽量
            $this->updateCacheViewCount($id, $ip);
        }
    }

    /**
     * 一段時間內,限制同一IP訪問,防止增長無效瀏覽次數
     * @param $id
     * @param $ip
     * @return bool
     */
    public function ipViewLimit($id, $ip)
    {
//        $ip = '1.1.1.6';
        //redis中鍵值分割都以:來作,能夠理解爲PHP的命名空間namespace同樣
        $ipPostViewKey    = 'post:ip:limit:'.$id;
        //Redis命令SISMEMBER檢查集合類型Set中有沒有該鍵,該指令時間複雜度O(1),Set集合類型中值都是惟一
        $existsInRedisSet = Redis::command('SISMEMBER', [$ipPostViewKey, $ip]);
        if(!$existsInRedisSet){
            //SADD,集合類型指令,向ipPostViewKey鍵中加一個值ip
            Redis::command('SADD', [$ipPostViewKey, $ip]);
            //並給該鍵設置生命時間,這裏設置300秒,300秒後同一IP訪問就當作是新的瀏覽量了
            Redis::command('EXPIRE', [$ipPostViewKey, self::ipExpireSec]);
            return true;
        }

        return false;
    }

    /**
     * 更新DB中post瀏覽次數
     * @param $id
     * @param $count
     */
    public function updateModelViewCount($id, $count)
    {
        //訪問量達到300,再進行一次SQL更新
        $postModel              = Post::find($id);
        $postModel->view_count += $count;
        $postModel->save();
    }

    /**
     * 不一樣用戶訪問,更新緩存中瀏覽次數
     * @param $id
     * @param $ip
     */
    public function updateCacheViewCount($id, $ip)
    {
        $cacheKey        = 'post:view:'.$id;
        //這裏以Redis哈希類型存儲鍵,就和數組相似,$cacheKey就相似數組名,$ip爲$key.HEXISTS指令判斷$key是否存在$cacheKey中
        if(Redis::command('HEXISTS', [$cacheKey, $ip])){
            //哈希類型指令HINCRBY,就是給$cacheKey[$ip]加上一個值,這裏一次訪問就是1
            $incre_count = Redis::command('HINCRBY', [$cacheKey, $ip, 1]);
            //redis中這個存儲瀏覽量的值達到30後,就往MySQL裏刷下,這樣就不須要每一次瀏覽,來一次query,效率不高
            if($incre_count == self::postViewLimit){
                $this->updateModelViewCount($id, $incre_count);
                //本篇post,redis中瀏覽量刷進MySQL後,把該篇post的瀏覽量鍵抹掉,等着下一次請求從新開始計數
                Redis::command('HDEL', [$cacheKey, $ip]);
                //同時,抹掉post內容的緩存鍵,這樣就不用等10分鐘後再更新view_count了,
                //如該篇post在100秒內就達到了30訪問量,就在3分鐘時更新下MySQL,並把緩存抹掉,下一次請求就從MySQL中請求到最新的view_count,
                //固然,100秒內view_count仍是緩存的舊數據,極端狀況300秒內都是舊數據,而緩存裏已經有了29個新增訪問量
                //實際上也能夠這樣作:在緩存post的時候,能夠把view_count單獨拿出來存入鍵值裏如single_view_count,每一次都是給這個值加1,而後把這個值傳入視圖裏
                //或者平衡設置下postViewLimit和ipExpireSec這兩個參數,對於view_count這種實時性要求不高的能夠這樣作來着
                //加上laravel前綴,由於Cache::remember會自動在每個key前加上laravel前綴,能夠看cache.php中這個字段:'prefix' => 'laravel'
                Redis::command('DEL', ['laravel:post:cache:'.$id]);
            }
        }else{
            //哈希類型指令HSET,和數組相似,就像$cacheKey[$ip] = 1;
            Redis::command('HSET', [$cacheKey, $ip, '1']);
        }
    }
}

這裏推薦下一本Redis入門書《Redis入門指南》(做者也是咱北航的,軟件學院的,竟然比我小一屆,慚愧。。不過俺們也參與寫過書,哈哈,只是參與,呵呵),快的話看個一兩天就能看完,也就基本入門了。還推薦一個Redis客戶端:Redis Desktop Manager,能夠在客戶端裏看下各個鍵值:

圖片描述
圖片描述
頁面視圖中能夠利用上面推薦的barryvdh/laravel-debugbar插件觀察下請求過程產生的數據。第一次請求時會有一次query,而後從緩存裏取值沒有query了,直到把緩存中view_count刷到MySQL裏再有一次query:
圖片描述

It is working!!!

不知道有沒有說清楚,有疑問或者指正的地方請留言交流吧。

總結:研究Redis和Cache模塊的時候,還看到能夠利用Model Observer模型觀察器來監聽事件自動刷新緩存,晚上在研究下吧,這兩天也順便把Redis數據存儲類型總結下,到時見。

歡迎關注Laravel-China

RightCapital招聘Laravel DevOps

相關文章
相關標籤/搜索